@query-doctor/core 0.8.1 → 0.8.2

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.
Files changed (118) hide show
  1. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.cjs +13 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs +13 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPrimitive.cjs +15 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPrimitive.mjs +15 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPropertyKey.cjs +10 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPropertyKey.mjs +10 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/typeof.cjs +17 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/typeof.mjs +12 -0
  9. package/dist/_virtual/_rolldown/runtime.cjs +24 -0
  10. package/dist/index.cjs +33 -2568
  11. package/dist/index.d.cts +11 -781
  12. package/dist/index.d.mts +11 -781
  13. package/dist/index.mjs +10 -2522
  14. package/dist/optimizer/genalgo.cjs +365 -0
  15. package/dist/optimizer/genalgo.cjs.map +1 -0
  16. package/dist/optimizer/genalgo.d.cts +111 -0
  17. package/dist/optimizer/genalgo.d.cts.map +1 -0
  18. package/dist/optimizer/genalgo.d.mts +111 -0
  19. package/dist/optimizer/genalgo.d.mts.map +1 -0
  20. package/dist/optimizer/genalgo.mjs +362 -0
  21. package/dist/optimizer/genalgo.mjs.map +1 -0
  22. package/dist/optimizer/pss-rewriter.cjs +31 -0
  23. package/dist/optimizer/pss-rewriter.cjs.map +1 -0
  24. package/dist/optimizer/pss-rewriter.d.cts +16 -0
  25. package/dist/optimizer/pss-rewriter.d.cts.map +1 -0
  26. package/dist/optimizer/pss-rewriter.d.mts +16 -0
  27. package/dist/optimizer/pss-rewriter.d.mts.map +1 -0
  28. package/dist/optimizer/pss-rewriter.mjs +31 -0
  29. package/dist/optimizer/pss-rewriter.mjs.map +1 -0
  30. package/dist/optimizer/statistics.cjs +738 -0
  31. package/dist/optimizer/statistics.cjs.map +1 -0
  32. package/dist/optimizer/statistics.d.cts +389 -0
  33. package/dist/optimizer/statistics.d.cts.map +1 -0
  34. package/dist/optimizer/statistics.d.mts +389 -0
  35. package/dist/optimizer/statistics.d.mts.map +1 -0
  36. package/dist/optimizer/statistics.mjs +729 -0
  37. package/dist/optimizer/statistics.mjs.map +1 -0
  38. package/dist/sentry.cjs +13 -0
  39. package/dist/sentry.cjs.map +1 -0
  40. package/dist/sentry.d.cts +7 -0
  41. package/dist/sentry.d.cts.map +1 -0
  42. package/dist/sentry.d.mts +7 -0
  43. package/dist/sentry.d.mts.map +1 -0
  44. package/dist/sentry.mjs +13 -0
  45. package/dist/sentry.mjs.map +1 -0
  46. package/dist/sql/analyzer.cjs +242 -0
  47. package/dist/sql/analyzer.cjs.map +1 -0
  48. package/dist/sql/analyzer.d.cts +112 -0
  49. package/dist/sql/analyzer.d.cts.map +1 -0
  50. package/dist/sql/analyzer.d.mts +112 -0
  51. package/dist/sql/analyzer.d.mts.map +1 -0
  52. package/dist/sql/analyzer.mjs +240 -0
  53. package/dist/sql/analyzer.mjs.map +1 -0
  54. package/dist/sql/ast-utils.cjs +19 -0
  55. package/dist/sql/ast-utils.cjs.map +1 -0
  56. package/dist/sql/ast-utils.d.cts +9 -0
  57. package/dist/sql/ast-utils.d.cts.map +1 -0
  58. package/dist/sql/ast-utils.d.mts +9 -0
  59. package/dist/sql/ast-utils.d.mts.map +1 -0
  60. package/dist/sql/ast-utils.mjs +17 -0
  61. package/dist/sql/ast-utils.mjs.map +1 -0
  62. package/dist/sql/builder.cjs +94 -0
  63. package/dist/sql/builder.cjs.map +1 -0
  64. package/dist/sql/builder.d.cts +37 -0
  65. package/dist/sql/builder.d.cts.map +1 -0
  66. package/dist/sql/builder.d.mts +37 -0
  67. package/dist/sql/builder.d.mts.map +1 -0
  68. package/dist/sql/builder.mjs +94 -0
  69. package/dist/sql/builder.mjs.map +1 -0
  70. package/dist/sql/database.cjs +35 -0
  71. package/dist/sql/database.cjs.map +1 -0
  72. package/dist/sql/database.d.cts +91 -0
  73. package/dist/sql/database.d.cts.map +1 -0
  74. package/dist/sql/database.d.mts +91 -0
  75. package/dist/sql/database.d.mts.map +1 -0
  76. package/dist/sql/database.mjs +32 -0
  77. package/dist/sql/database.mjs.map +1 -0
  78. package/dist/sql/indexes.cjs +17 -0
  79. package/dist/sql/indexes.cjs.map +1 -0
  80. package/dist/sql/indexes.d.cts +14 -0
  81. package/dist/sql/indexes.d.cts.map +1 -0
  82. package/dist/sql/indexes.d.mts +14 -0
  83. package/dist/sql/indexes.d.mts.map +1 -0
  84. package/dist/sql/indexes.mjs +16 -0
  85. package/dist/sql/indexes.mjs.map +1 -0
  86. package/dist/sql/nudges.cjs +484 -0
  87. package/dist/sql/nudges.cjs.map +1 -0
  88. package/dist/sql/nudges.d.cts +21 -0
  89. package/dist/sql/nudges.d.cts.map +1 -0
  90. package/dist/sql/nudges.d.mts +21 -0
  91. package/dist/sql/nudges.d.mts.map +1 -0
  92. package/dist/sql/nudges.mjs +484 -0
  93. package/dist/sql/nudges.mjs.map +1 -0
  94. package/dist/sql/permutations.cjs +28 -0
  95. package/dist/sql/permutations.cjs.map +1 -0
  96. package/dist/sql/permutations.mjs +28 -0
  97. package/dist/sql/permutations.mjs.map +1 -0
  98. package/dist/sql/pg-identifier.cjs +216 -0
  99. package/dist/sql/pg-identifier.cjs.map +1 -0
  100. package/dist/sql/pg-identifier.d.cts +31 -0
  101. package/dist/sql/pg-identifier.d.cts.map +1 -0
  102. package/dist/sql/pg-identifier.d.mts +31 -0
  103. package/dist/sql/pg-identifier.d.mts.map +1 -0
  104. package/dist/sql/pg-identifier.mjs +216 -0
  105. package/dist/sql/pg-identifier.mjs.map +1 -0
  106. package/dist/sql/walker.cjs +314 -0
  107. package/dist/sql/walker.cjs.map +1 -0
  108. package/dist/sql/walker.d.cts +15 -0
  109. package/dist/sql/walker.d.cts.map +1 -0
  110. package/dist/sql/walker.d.mts +15 -0
  111. package/dist/sql/walker.d.mts.map +1 -0
  112. package/dist/sql/walker.mjs +313 -0
  113. package/dist/sql/walker.mjs.map +1 -0
  114. package/package.json +2 -1
  115. package/dist/index.cjs.map +0 -1
  116. package/dist/index.d.cts.map +0 -1
  117. package/dist/index.d.mts.map +0 -1
  118. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,362 @@
1
+ "use client";
2
+ import { _defineProperty } from "../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs";
3
+ import { dropIndex } from "../sql/database.mjs";
4
+ import { isIndexProbablyDroppable } from "../sql/indexes.mjs";
5
+ import { PostgresQueryBuilder } from "../sql/builder.mjs";
6
+ import { PgIdentifier } from "../sql/pg-identifier.mjs";
7
+ import { permutationsWithDescendingLength } from "../sql/permutations.mjs";
8
+ import { blue, gray, green, magenta, red, yellow } from "colorette";
9
+ //#region src/optimizer/genalgo.ts
10
+ var IndexOptimizer = class IndexOptimizer {
11
+ constructor(db, statistics, existingIndexes, config = {}) {
12
+ this.db = db;
13
+ this.statistics = statistics;
14
+ this.existingIndexes = existingIndexes;
15
+ this.config = config;
16
+ }
17
+ async run(builder, indexes, beforeQuery) {
18
+ const baseExplain = await this.testQueryWithStats(builder, async (tx) => {
19
+ if (beforeQuery) await beforeQuery(tx);
20
+ });
21
+ const baseCost = Number(baseExplain.Plan["Total Cost"]);
22
+ if (baseCost === 0) return {
23
+ kind: "zero_cost_plan",
24
+ explainPlan: baseExplain.Plan
25
+ };
26
+ const toCreate = this.indexesToCreate(indexes);
27
+ if (toCreate.length === 0) {
28
+ const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
29
+ return {
30
+ kind: "ok",
31
+ baseCost,
32
+ finalCost: baseCost,
33
+ newIndexes: /* @__PURE__ */ new Set(),
34
+ existingIndexes: baseIndexes.existingIndexes,
35
+ triedIndexes: /* @__PURE__ */ new Map(),
36
+ baseExplainPlan: baseExplain.Plan,
37
+ explainPlan: baseExplain.Plan
38
+ };
39
+ }
40
+ const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
41
+ if (beforeQuery) await beforeQuery(tx);
42
+ for (const permutation of toCreate) {
43
+ const createIndex = PostgresQueryBuilder.createIndex(permutation.definition, permutation.name).introspect().build();
44
+ await tx.exec(createIndex);
45
+ }
46
+ });
47
+ const finalCost = Number(finalExplain.Plan["Total Cost"]);
48
+ if (this.config.debug) console.dir(finalExplain, { depth: null });
49
+ const deltaPercentage = (baseCost - finalCost) / baseCost * 100;
50
+ if (finalCost < baseCost) console.log(` 🎉🎉🎉 ${green(`+${deltaPercentage.toFixed(2).padStart(5, "0")}%`)}`);
51
+ else if (finalCost > baseCost) console.log(`${red(`-${Math.abs(deltaPercentage).toFixed(2).padStart(5, "0")}%`)} ${gray("If there's a better index, we haven't tried it")}`);
52
+ const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
53
+ const finalIndexes = this.findUsedIndexes(finalExplain.Plan);
54
+ const triedIndexes = new Map(toCreate.map((index) => [index.name.toString(), index]));
55
+ this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);
56
+ return {
57
+ kind: "ok",
58
+ baseCost,
59
+ finalCost,
60
+ newIndexes: finalIndexes.newIndexes,
61
+ existingIndexes: baseIndexes.existingIndexes,
62
+ triedIndexes,
63
+ baseExplainPlan: baseExplain.Plan,
64
+ explainPlan: finalExplain.Plan
65
+ };
66
+ }
67
+ async runWithoutIndexes(builder) {
68
+ return await this.testQueryWithStats(builder, async (tx) => {
69
+ await this.dropExistingIndexes(tx);
70
+ });
71
+ }
72
+ /**
73
+ * Given the current indexes in the optimizer, transform them in some
74
+ * way to change which indexes will be assumed to exist when optimizing
75
+ *
76
+ * @example
77
+ * ```
78
+ * // resets indexes
79
+ * optimizer.transformIndexes(() => [])
80
+ *
81
+ * // adds new index
82
+ * optimizer.transformIndexes(indexes => [...indexes, newIndex])
83
+ * ```
84
+ */
85
+ transformIndexes(f) {
86
+ this.existingIndexes = f(this.existingIndexes);
87
+ return this;
88
+ }
89
+ /**
90
+ * Postgres has a limit of 63 characters for index names.
91
+ * So we use this to make sure we don't derive it from a list of columns that can
92
+ * overflow that limit.
93
+ */
94
+ indexName() {
95
+ const indexName = IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);
96
+ return PgIdentifier.fromString(indexName);
97
+ }
98
+ indexAlreadyExists(table, columns) {
99
+ return this.existingIndexes.find((index) => index.index_type === "btree" && index.table_name === table && index.index_columns.length === columns.length && index.index_columns.every((c, i) => {
100
+ if (columns[i].column !== c.name) return false;
101
+ if (columns[i].where) return false;
102
+ if (columns[i].sort) switch (columns[i].sort.dir) {
103
+ case "SORTBY_DEFAULT":
104
+ case "SORTBY_ASC":
105
+ if (c.order !== "ASC") return false;
106
+ break;
107
+ case "SORTBY_DESC":
108
+ if (c.order !== "DESC") return false;
109
+ break;
110
+ }
111
+ return true;
112
+ }));
113
+ }
114
+ /**
115
+ * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
116
+ **/
117
+ indexesToCreate(rootCandidates) {
118
+ const expressionCandidates = rootCandidates.filter((c) => c.jsonbExtraction);
119
+ const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator && !c.jsonbExtraction);
120
+ const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
121
+ const nextStage = [];
122
+ const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
123
+ for (const permutation of permutedIndexes.values()) {
124
+ const { table: rawTable, schema: rawSchema, columns } = permutation;
125
+ const permutations = permutationsWithDescendingLength(columns);
126
+ for (const columns of permutations) {
127
+ const schema = PgIdentifier.fromString(rawSchema);
128
+ const table = PgIdentifier.fromString(rawTable);
129
+ if (this.indexAlreadyExists(table.toString(), columns)) continue;
130
+ const indexName = this.indexName();
131
+ const definition = this.toDefinition({
132
+ table,
133
+ schema,
134
+ columns
135
+ }).raw;
136
+ nextStage.push({
137
+ name: indexName,
138
+ schema: schema.toString(),
139
+ table: table.toString(),
140
+ columns,
141
+ definition
142
+ });
143
+ }
144
+ }
145
+ const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
146
+ for (const group of ginGroups.values()) {
147
+ const { schema: rawSchema, table: rawTable, column, operators } = group;
148
+ const schema = PgIdentifier.fromString(rawSchema);
149
+ const table = PgIdentifier.fromString(rawTable);
150
+ const opclass = operators.some((op) => op !== "@>") ? void 0 : "jsonb_path_ops";
151
+ if (this.ginIndexAlreadyExists(table.toString(), column)) continue;
152
+ const indexName = this.indexName();
153
+ const candidate = {
154
+ schema: rawSchema,
155
+ table: rawTable,
156
+ column
157
+ };
158
+ const definition = this.toGinDefinition({
159
+ table,
160
+ schema,
161
+ column: PgIdentifier.fromString(column),
162
+ opclass
163
+ });
164
+ nextStage.push({
165
+ name: indexName,
166
+ schema: schema.toString(),
167
+ table: table.toString(),
168
+ columns: [candidate],
169
+ definition,
170
+ indexMethod: "gin",
171
+ opclass
172
+ });
173
+ }
174
+ const seenExpressions = /* @__PURE__ */ new Set();
175
+ for (const candidate of expressionCandidates) {
176
+ const expression = candidate.jsonbExtraction;
177
+ const key = `${candidate.schema}.${candidate.table}.${expression}`;
178
+ if (seenExpressions.has(key)) continue;
179
+ seenExpressions.add(key);
180
+ const schema = PgIdentifier.fromString(candidate.schema);
181
+ const table = PgIdentifier.fromString(candidate.table);
182
+ const indexName = this.indexName();
183
+ const definition = this.toExpressionDefinition({
184
+ table,
185
+ schema,
186
+ expression
187
+ });
188
+ nextStage.push({
189
+ name: indexName,
190
+ schema: schema.toString(),
191
+ table: table.toString(),
192
+ columns: [candidate],
193
+ definition
194
+ });
195
+ }
196
+ return nextStage;
197
+ }
198
+ toDefinition({ schema, table, columns }) {
199
+ const make = (col, order, _where, _keyword) => {
200
+ let fullyQualifiedTable;
201
+ if (schema.toString() === "public") fullyQualifiedTable = table;
202
+ else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
203
+ return `${fullyQualifiedTable}(${columns.map((c) => {
204
+ const column = PgIdentifier.fromString(c.column);
205
+ const direction = c.sort && this.sortDirection(c.sort);
206
+ const nulls = c.sort && this.nullsOrder(c.sort);
207
+ let sort = col(column.toString());
208
+ if (direction) sort += ` ${order(direction)}`;
209
+ if (nulls) sort += ` ${order(nulls)}`;
210
+ return sort;
211
+ }).join(", ")})`;
212
+ };
213
+ const id = (a) => a;
214
+ return {
215
+ raw: make(id, id, id, id),
216
+ colored: make(green, yellow, magenta, blue)
217
+ };
218
+ }
219
+ toGinDefinition({ schema, table, column, opclass }) {
220
+ let fullyQualifiedTable;
221
+ if (schema.toString() === "public") fullyQualifiedTable = table;
222
+ else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
223
+ const opclassSuffix = opclass ? ` ${opclass}` : "";
224
+ return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
225
+ }
226
+ toExpressionDefinition({ schema, table, expression }) {
227
+ let fullyQualifiedTable;
228
+ if (schema.toString() === "public") fullyQualifiedTable = table;
229
+ else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
230
+ return `${fullyQualifiedTable}((${expression}))`;
231
+ }
232
+ groupGinCandidatesByColumn(candidates) {
233
+ const groups = /* @__PURE__ */ new Map();
234
+ for (const c of candidates) {
235
+ if (!c.jsonbOperator) continue;
236
+ const key = `${c.schema}.${c.table}.${c.column}`;
237
+ const existing = groups.get(key);
238
+ if (existing) {
239
+ if (!existing.operators.includes(c.jsonbOperator)) existing.operators.push(c.jsonbOperator);
240
+ } else groups.set(key, {
241
+ schema: c.schema,
242
+ table: c.table,
243
+ column: c.column,
244
+ operators: [c.jsonbOperator]
245
+ });
246
+ }
247
+ return groups;
248
+ }
249
+ ginIndexAlreadyExists(table, column) {
250
+ return this.existingIndexes.find((index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column));
251
+ }
252
+ /**
253
+ * Drop indexes that can be dropped. Ignore the ones that can't
254
+ */
255
+ async dropExistingIndexes(tx) {
256
+ for (const index of this.existingIndexes) {
257
+ if (!isIndexProbablyDroppable(index)) continue;
258
+ await dropIndex(tx, PgIdentifier.fromParts(index.schema_name, index.index_name));
259
+ }
260
+ }
261
+ whereClause(c, col, keyword) {
262
+ if (!c.where) return "";
263
+ if (c.where.nulltest === "IS_NULL") return `${col(`"${c.column}"`)} is ${keyword("null")}`;
264
+ if (c.where.nulltest === "IS_NOT_NULL") return `${col(`"${c.column}"`)} is not ${keyword("null")}`;
265
+ return "";
266
+ }
267
+ nullsOrder(s) {
268
+ if (!s.nulls) return "";
269
+ switch (s.nulls) {
270
+ case "SORTBY_NULLS_FIRST": return "nulls first";
271
+ case "SORTBY_NULLS_LAST": return "nulls last";
272
+ default: return "";
273
+ }
274
+ }
275
+ sortDirection(s) {
276
+ if (!s.dir) return "";
277
+ switch (s.dir) {
278
+ case "SORTBY_DESC": return "desc";
279
+ case "SORTBY_ASC": return "asc";
280
+ default: return "";
281
+ }
282
+ }
283
+ async testQueryWithStats(builder, f, options) {
284
+ try {
285
+ await this.db.transaction(async (tx) => {
286
+ await f?.(tx);
287
+ await this.statistics.restoreStats(tx);
288
+ const flags = ["format json"];
289
+ if (options && !options.genericPlan) {
290
+ flags.push("analyze");
291
+ if (this.config.trace) flags.push("trace");
292
+ } else flags.push("generic_plan");
293
+ const { commands, query } = builder.introspect().explain(flags).buildParts();
294
+ await tx.exec(commands);
295
+ const explain = (await tx.exec(query, options?.params))[0]["QUERY PLAN"][0];
296
+ throw new RollbackError(explain);
297
+ });
298
+ } catch (error) {
299
+ if (error instanceof RollbackError) return error.value;
300
+ throw error;
301
+ }
302
+ throw new Error("Unreachable");
303
+ }
304
+ groupPotentialIndexColumnsByTable(indexes) {
305
+ const tableColumns = /* @__PURE__ */ new Map();
306
+ for (const index of indexes) {
307
+ const existing = tableColumns.get(`${index.schema}.${index.table}`);
308
+ if (existing) existing.columns.push(index);
309
+ else tableColumns.set(`${index.schema}.${index.table}`, {
310
+ table: index.table,
311
+ schema: index.schema,
312
+ columns: [index]
313
+ });
314
+ }
315
+ return tableColumns;
316
+ }
317
+ findUsedIndexes(explain) {
318
+ const newIndexes = /* @__PURE__ */ new Set();
319
+ const existingIndexes = /* @__PURE__ */ new Set();
320
+ const prefix = IndexOptimizer.prefix;
321
+ walkExplain(explain, (stage) => {
322
+ const indexName = stage["Index Name"];
323
+ if (indexName) if (indexName.startsWith(prefix)) newIndexes.add(indexName);
324
+ else if (indexName.includes(prefix)) {
325
+ const actualName = indexName.substring(indexName.indexOf(prefix));
326
+ newIndexes.add(actualName);
327
+ } else existingIndexes.add(indexName);
328
+ });
329
+ return {
330
+ newIndexes,
331
+ existingIndexes
332
+ };
333
+ }
334
+ replaceUsedIndexesWithDefinition(explain, triedIndexes) {
335
+ walkExplain(explain, (stage) => {
336
+ const indexName = stage["Index Name"];
337
+ if (typeof indexName === "string") {
338
+ const recommendation = triedIndexes.get(indexName);
339
+ if (recommendation) stage["Index Name"] = recommendation.definition;
340
+ }
341
+ });
342
+ }
343
+ };
344
+ _defineProperty(IndexOptimizer, "prefix", "__qd_");
345
+ function walkExplain(explain, f) {
346
+ function go(plan) {
347
+ f(plan);
348
+ if (plan.Plans) for (const p of plan.Plans) go(p);
349
+ }
350
+ go(explain);
351
+ }
352
+ var RollbackError = class {
353
+ constructor(value) {
354
+ this.value = value;
355
+ }
356
+ };
357
+ const PROCEED = Symbol("PROCEED");
358
+ const SKIP = Symbol("SKIP");
359
+ //#endregion
360
+ export { IndexOptimizer, PROCEED, SKIP };
361
+
362
+ //# sourceMappingURL=genalgo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genalgo.mjs","names":[],"sources":["../../src/optimizer/genalgo.ts"],"sourcesContent":["import type { NullTestType } from \"@pgsql/types\";\nimport { blue, gray, green, magenta, red, yellow } from \"colorette\";\nimport type { JsonbOperator } from \"../sql/analyzer.js\";\nimport type { SortContext } from \"../sql/analyzer.js\";\nimport { PostgresQueryBuilder } from \"../sql/builder.js\";\nimport {\n dropIndex,\n type Postgres,\n type PostgresExplainResult,\n type PostgresExplainStage,\n type PostgresTransaction,\n} from \"../sql/database.js\";\nimport { isIndexProbablyDroppable } from \"../sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"../sql/permutations.js\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexedTable, Statistics } from \"./statistics.js\";\n\nexport type IndexIdentifier = string;\n\nexport type IndexRecommendation = PermutedIndexCandidate & {\n definition: IndexIdentifier;\n};\n\ntype Color = (a: string) => string;\n\ntype IndexToCreate = PermutedIndexCandidate & {\n name: PgIdentifier;\n definition: IndexIdentifier;\n};\n\nexport class IndexOptimizer {\n static prefix = \"__qd_\";\n\n constructor(\n private readonly db: Postgres,\n private readonly statistics: Statistics,\n private existingIndexes: IndexedTable[],\n private readonly config: {\n trace?: boolean;\n debug?: boolean;\n } = {},\n ) {}\n\n async run(\n builder: PostgresQueryBuilder,\n indexes: RootIndexCandidate[],\n beforeQuery?: (tx: PostgresTransaction) => Promise<void>,\n ): Promise<OptimizeResult> {\n const baseExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n });\n const baseCost: number = Number(baseExplain.Plan[\"Total Cost\"]);\n if (baseCost === 0) {\n return {\n kind: \"zero_cost_plan\",\n explainPlan: baseExplain.Plan,\n };\n }\n const toCreate = this.indexesToCreate(indexes);\n if (toCreate.length === 0) {\n // No indexes to try: the 2nd EXPLAIN would be identical to the 1st,\n // so skip it. On large tables this saves a full re-run of the query.\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n return {\n kind: \"ok\",\n baseCost,\n finalCost: baseCost,\n newIndexes: new Set<string>(),\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes: new Map(),\n baseExplainPlan: baseExplain.Plan,\n explainPlan: baseExplain.Plan,\n };\n }\n const finalExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n\n // Then create recommended indexes\n for (const permutation of toCreate) {\n const createIndex = PostgresQueryBuilder.createIndex(\n permutation.definition,\n permutation.name,\n )\n .introspect()\n .build();\n\n await tx.exec(createIndex);\n }\n });\n const finalCost = Number(finalExplain.Plan[\"Total Cost\"]);\n if (this.config.debug) {\n console.dir(finalExplain, { depth: null });\n }\n const deltaPercentage = ((baseCost - finalCost) / baseCost) * 100;\n if (finalCost < baseCost) {\n console.log(\n ` 🎉🎉🎉 ${green(`+${deltaPercentage.toFixed(2).padStart(5, \"0\")}%`)}`,\n );\n } else if (finalCost > baseCost) {\n console.log(\n `${red(\n `-${Math.abs(deltaPercentage).toFixed(2).padStart(5, \"0\")}%`,\n )} ${gray(\"If there's a better index, we haven't tried it\")}`,\n );\n }\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n const finalIndexes = this.findUsedIndexes(finalExplain.Plan);\n const triedIndexes = new Map(\n toCreate.map((index) => [index.name.toString(), index]),\n );\n this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);\n\n return {\n kind: \"ok\",\n baseCost,\n finalCost,\n newIndexes: finalIndexes.newIndexes,\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes,\n baseExplainPlan: baseExplain.Plan,\n explainPlan: finalExplain.Plan,\n };\n }\n\n async runWithoutIndexes(builder: PostgresQueryBuilder) {\n return await this.testQueryWithStats(builder, async (tx) => {\n await this.dropExistingIndexes(tx);\n });\n }\n\n /**\n * Given the current indexes in the optimizer, transform them in some\n * way to change which indexes will be assumed to exist when optimizing\n *\n * @example\n * ```\n * // resets indexes\n * optimizer.transformIndexes(() => [])\n *\n * // adds new index\n * optimizer.transformIndexes(indexes => [...indexes, newIndex])\n * ```\n */\n transformIndexes(f: (indexes: IndexedTable[]) => IndexedTable[]) {\n const newIndexes = f(this.existingIndexes);\n this.existingIndexes = newIndexes;\n return this;\n }\n\n /**\n * Postgres has a limit of 63 characters for index names.\n * So we use this to make sure we don't derive it from a list of columns that can\n * overflow that limit.\n */\n private indexName(): PgIdentifier {\n const indexName =\n IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);\n return PgIdentifier.fromString(indexName);\n }\n\n // TODO: this doesn't belong in the optimizer\n private indexAlreadyExists(\n table: string,\n columns: RootIndexCandidate[],\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"btree\" &&\n index.table_name === table &&\n index.index_columns.length === columns.length &&\n index.index_columns.every((c, i) => {\n if (columns[i].column !== c.name) {\n return false;\n }\n\n // we should assume any index with `WHERE`\n // can't be counted as a duplicate\n if (columns[i].where) {\n return false;\n }\n\n if (columns[i].sort) {\n switch (columns[i].sort.dir) {\n // Sorting is ASC by default in postgres\n case \"SORTBY_DEFAULT\":\n case \"SORTBY_ASC\":\n if (c.order !== \"ASC\") {\n return false;\n }\n break;\n case \"SORTBY_DESC\":\n if (c.order !== \"DESC\") {\n return false;\n }\n break;\n }\n }\n return true;\n }),\n );\n }\n\n /**\n * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]\n **/\n private indexesToCreate(\n rootCandidates: RootIndexCandidate[],\n ): IndexToCreate[] {\n const expressionCandidates = rootCandidates.filter(\n (c) => c.jsonbExtraction,\n );\n const btreeCandidates = rootCandidates.filter(\n (c) => !c.jsonbOperator && !c.jsonbExtraction,\n );\n const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);\n\n const nextStage: IndexToCreate[] = [];\n\n const permutedIndexes =\n this.groupPotentialIndexColumnsByTable(btreeCandidates);\n for (const permutation of permutedIndexes.values()) {\n const { table: rawTable, schema: rawSchema, columns } = permutation;\n const permutations = permutationsWithDescendingLength(columns);\n for (const columns of permutations) {\n // TODO: accept PgIdentifier values instead\n // required refactoring `PermutedIndexCandidate`\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n const existingIndex = this.indexAlreadyExists(\n table.toString(),\n columns,\n );\n if (existingIndex) {\n continue;\n }\n const indexName = this.indexName();\n\n const definition = this.toDefinition({ table, schema, columns }).raw;\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns,\n definition,\n });\n }\n }\n\n const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);\n for (const group of ginGroups.values()) {\n const { schema: rawSchema, table: rawTable, column, operators } = group;\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n\n // jsonb_path_ops is smaller/faster but only supports @>.\n // All other operators (key-existence and jsonpath) need the full jsonb_ops.\n const needsFullOps = operators.some((op) => op !== \"@>\");\n const opclass = needsFullOps ? undefined : \"jsonb_path_ops\";\n\n const existingGin = this.ginIndexAlreadyExists(table.toString(), column);\n if (existingGin) {\n continue;\n }\n\n const indexName = this.indexName();\n const candidate: RootIndexCandidate = {\n schema: rawSchema,\n table: rawTable,\n column,\n };\n const definition = this.toGinDefinition({\n table,\n schema,\n column: PgIdentifier.fromString(column),\n opclass,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n indexMethod: \"gin\",\n opclass,\n });\n }\n\n // Expression B-tree indexes for JSONB path extraction (-> / ->>)\n const seenExpressions = new Set<string>();\n for (const candidate of expressionCandidates) {\n const expression = candidate.jsonbExtraction!;\n const key = `${candidate.schema}.${candidate.table}.${expression}`;\n if (seenExpressions.has(key)) continue;\n seenExpressions.add(key);\n\n const schema = PgIdentifier.fromString(candidate.schema);\n const table = PgIdentifier.fromString(candidate.table);\n const indexName = this.indexName();\n const definition = this.toExpressionDefinition({\n table,\n schema,\n expression,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n });\n }\n\n return nextStage;\n }\n\n private toDefinition({\n schema,\n table,\n columns,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n columns: RootIndexCandidate[];\n }) {\n const make = (col: Color, order: Color, _where: Color, _keyword: Color) => {\n // TODO: refactor all of this class to accept PgIdentifiers\n let fullyQualifiedTable: PgIdentifier;\n\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const baseColumn = `${fullyQualifiedTable}(${columns\n .map((c) => {\n const column = PgIdentifier.fromString(c.column);\n const direction = c.sort && this.sortDirection(c.sort);\n const nulls = c.sort && this.nullsOrder(c.sort);\n let sort = col(column.toString());\n if (direction) {\n sort += ` ${order(direction)}`;\n }\n if (nulls) {\n sort += ` ${order(nulls)}`;\n }\n return sort;\n })\n .join(\", \")})`;\n // TODO: add support for generating partial indexes\n // if (clauses.length > 0) {\n // return `${baseColumn} ${where(\"where\")} ${clauses.join(\" and \")}`;\n // }\n return baseColumn;\n };\n const id: Color = (a) => a;\n const raw = make(id, id, id, id);\n const colored = make(green, yellow, magenta, blue);\n return { raw, colored };\n }\n\n private toGinDefinition({\n schema,\n table,\n column,\n opclass,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n column: PgIdentifier;\n opclass?: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const opclassSuffix = opclass ? ` ${opclass}` : \"\";\n return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;\n }\n\n private toExpressionDefinition({\n schema,\n table,\n expression,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n expression: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n return `${fullyQualifiedTable}((${expression}))`;\n }\n\n private groupGinCandidatesByColumn(candidates: RootIndexCandidate[]) {\n const groups = new Map<\n string,\n {\n schema: string;\n table: string;\n column: string;\n operators: JsonbOperator[];\n }\n >();\n for (const c of candidates) {\n if (!c.jsonbOperator) continue;\n const key = `${c.schema}.${c.table}.${c.column}`;\n const existing = groups.get(key);\n if (existing) {\n if (!existing.operators.includes(c.jsonbOperator)) {\n existing.operators.push(c.jsonbOperator);\n }\n } else {\n groups.set(key, {\n schema: c.schema,\n table: c.table,\n column: c.column,\n operators: [c.jsonbOperator],\n });\n }\n }\n return groups;\n }\n\n private ginIndexAlreadyExists(\n table: string,\n column: string,\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"gin\" &&\n index.table_name === table &&\n index.index_columns.some((c) => c.name === column),\n );\n }\n\n /**\n * Drop indexes that can be dropped. Ignore the ones that can't\n */\n private async dropExistingIndexes(tx: PostgresTransaction) {\n for (const index of this.existingIndexes) {\n if (!isIndexProbablyDroppable(index)) {\n continue;\n }\n const indexName = PgIdentifier.fromParts(\n index.schema_name,\n index.index_name,\n );\n await dropIndex(tx, indexName);\n }\n }\n\n private whereClause(c: RootIndexCandidate, col: Color, keyword: Color) {\n if (!c.where) {\n return \"\";\n }\n if (c.where.nulltest === \"IS_NULL\") {\n return `${col(`\"${c.column}\"`)} is ${keyword(\"null\")}`;\n }\n if (c.where.nulltest === \"IS_NOT_NULL\") {\n return `${col(`\"${c.column}\"`)} is not ${keyword(\"null\")}`;\n }\n return \"\";\n }\n\n private nullsOrder(s: SortContext) {\n if (!s.nulls) {\n return \"\";\n }\n switch (s.nulls) {\n case \"SORTBY_NULLS_FIRST\":\n return \"nulls first\";\n case \"SORTBY_NULLS_LAST\":\n return \"nulls last\";\n case \"SORTBY_NULLS_DEFAULT\":\n default:\n return \"\";\n }\n }\n\n private sortDirection(s: SortContext) {\n if (!s.dir) {\n return \"\";\n }\n switch (s.dir) {\n case \"SORTBY_DESC\":\n return \"desc\";\n case \"SORTBY_ASC\":\n return \"asc\";\n case \"SORTBY_DEFAULT\":\n // god help us if we ever run into this\n case \"SORTBY_USING\":\n default:\n return \"\";\n }\n }\n\n async testQueryWithStats(\n builder: PostgresQueryBuilder,\n f?: (tx: PostgresTransaction) => Promise<void>,\n options?: { params?: unknown[]; genericPlan?: boolean },\n ): Promise<{ Plan: PostgresExplainStage }> {\n try {\n await this.db.transaction(async (tx) => {\n await f?.(tx);\n await this.statistics.restoreStats(tx);\n const flags = [\"format json\"];\n if (options && !options.genericPlan) {\n flags.push(\"analyze\");\n if (this.config.trace) {\n // trace can only be used alongside analyze\n // since it depends on the results of the query execution\n flags.push(\"trace\");\n }\n } else {\n flags.push(\"generic_plan\");\n }\n const { commands, query } = builder\n .introspect()\n .explain(flags)\n .buildParts();\n // this is done in a separate step to prevent sending multiple commands when using parameters\n await tx.exec(commands);\n const result = await tx.exec<PostgresExplainResult>(\n query,\n options?.params,\n );\n const explain = result[0][\"QUERY PLAN\"][0];\n throw new RollbackError(explain);\n });\n } catch (error) {\n if (error instanceof RollbackError) {\n return error.value;\n }\n throw error;\n }\n throw new Error(\"Unreachable\");\n }\n\n private groupPotentialIndexColumnsByTable(indexes: RootIndexCandidate[]) {\n const tableColumns: Map<\n string,\n { schema: string; table: string; columns: RootIndexCandidate[] }\n > = new Map();\n for (const index of indexes) {\n const existing = tableColumns.get(`${index.schema}.${index.table}`);\n if (existing) {\n existing.columns.push(index);\n } else {\n tableColumns.set(`${index.schema}.${index.table}`, {\n table: index.table,\n schema: index.schema,\n columns: [index],\n });\n }\n }\n return tableColumns;\n }\n\n private findUsedIndexes(explain: Record<string, any>) {\n const newIndexes: Set<string> = new Set();\n const existingIndexes: Set<string> = new Set();\n const prefix = IndexOptimizer.prefix;\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (indexName) {\n // Check for prefix at start or embedded (for hypertable chunk indexes like _hyper_1_1_chunk___qd_xxx)\n if (indexName.startsWith(prefix)) {\n newIndexes.add(indexName);\n } else if (indexName.includes(prefix)) {\n // Extract the actual index name from chunk-prefixed names (e.g., _hyper_1_1_chunk___qd_xxx -> __qd_xxx)\n const actualName = indexName.substring(indexName.indexOf(prefix));\n newIndexes.add(actualName);\n } else {\n existingIndexes.add(indexName);\n }\n }\n });\n return {\n newIndexes,\n existingIndexes,\n };\n }\n\n private replaceUsedIndexesWithDefinition(\n explain: Record<string, any>,\n triedIndexes: Map<string, IndexRecommendation>,\n ) {\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (typeof indexName === \"string\") {\n const recommendation = triedIndexes.get(indexName);\n if (recommendation) {\n stage[\"Index Name\"] = recommendation.definition;\n }\n }\n });\n }\n}\n\nfunction walkExplain(explain: Record<string, any>, f: (stage: any) => void) {\n function go(plan: any) {\n f(plan);\n if (plan.Plans) {\n for (const p of plan.Plans) {\n go(p);\n }\n }\n }\n go(explain);\n}\n\nexport type OptimizeResult =\n | {\n kind: \"ok\";\n baseExplainPlan: PostgresExplainStage;\n baseCost: number;\n finalCost: number;\n newIndexes: Set<string>;\n existingIndexes: Set<string>;\n triedIndexes: Map<string, IndexRecommendation>;\n explainPlan: PostgresExplainStage;\n }\n | {\n kind: \"zero_cost_plan\";\n explainPlan: PostgresExplainStage;\n };\n\nclass RollbackError<T> {\n constructor(public readonly value?: T) {}\n}\n\nexport type RootIndexCandidate = {\n schema: string;\n table: string;\n column: string;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n};\n\nexport type PermutedIndexCandidate = {\n schema: string;\n table: string;\n columns: RootIndexCandidate[];\n // TODO: functional indexes\n where?: string;\n indexMethod?: \"btree\" | \"gin\";\n opclass?: string;\n};\n\nexport const PROCEED = Symbol(\"PROCEED\");\nexport const SKIP = Symbol(\"SKIP\");\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ const require_defineProperty = require("../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.cjs");
3
+ //#region src/optimizer/pss-rewriter.ts
4
+ /**
5
+ * Rewriter for pg_stat_statements queries.
6
+ * Not all queries found in pg_stat_statements can be
7
+ * directly sent back to the database without first being rewritten.
8
+ */
9
+ var PssRewriter = class {
10
+ constructor() {
11
+ require_defineProperty._defineProperty(this, "problematicKeywords", [
12
+ "interval",
13
+ "timestamp",
14
+ "geometry"
15
+ ]);
16
+ }
17
+ rewrite(query) {
18
+ return this.rewriteKeywordWithParameter(query);
19
+ }
20
+ rewriteKeywordWithParameter(query) {
21
+ return query.replace(/\b(\w+) (\$\d+)\b/gi, (match) => {
22
+ const [keyword, parameter] = match.split(" ");
23
+ if (!this.problematicKeywords.includes(keyword.toLowerCase())) return match;
24
+ return `(${parameter}::${keyword.toLowerCase()})`;
25
+ });
26
+ }
27
+ };
28
+ //#endregion
29
+ exports.PssRewriter = PssRewriter;
30
+
31
+ //# sourceMappingURL=pss-rewriter.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pss-rewriter.cjs","names":[],"sources":["../../src/optimizer/pss-rewriter.ts"],"sourcesContent":["/**\n * Rewriter for pg_stat_statements queries.\n * Not all queries found in pg_stat_statements can be\n * directly sent back to the database without first being rewritten.\n */\nexport class PssRewriter {\n rewrite(query: string): string {\n return this.rewriteKeywordWithParameter(query);\n }\n\n private rewriteKeywordWithParameter(query: string): string {\n return query.replace(/\\b(\\w+) (\\$\\d+)\\b/gi, (match) => {\n const [keyword, parameter] = match.split(\" \");\n const isProblematicKeyword = this.problematicKeywords.includes(\n keyword.toLowerCase(),\n );\n if (!isProblematicKeyword) {\n return match;\n }\n return `(${parameter}::${keyword.toLowerCase()})`;\n });\n }\n\n private problematicKeywords = [\"interval\", \"timestamp\", \"geometry\"];\n}\n"],"mappings":";;;;;;;;AAKA,IAAa,cAAb,MAAyB;;+CAkBf,uBAAsB;GAAC;GAAY;GAAa;GAAW,CAAC;;CAjBpE,QAAQ,OAAuB;AAC7B,SAAO,KAAK,4BAA4B,MAAM;;CAGhD,4BAAoC,OAAuB;AACzD,SAAO,MAAM,QAAQ,wBAAwB,UAAU;GACrD,MAAM,CAAC,SAAS,aAAa,MAAM,MAAM,IAAI;AAI7C,OAAI,CAHyB,KAAK,oBAAoB,SACpD,QAAQ,aAAa,CACtB,CAEC,QAAO;AAET,UAAO,IAAI,UAAU,IAAI,QAAQ,aAAa,CAAC;IAC/C"}
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ //#region src/optimizer/pss-rewriter.d.ts
4
+ /**
5
+ * Rewriter for pg_stat_statements queries.
6
+ * Not all queries found in pg_stat_statements can be
7
+ * directly sent back to the database without first being rewritten.
8
+ */
9
+ declare class PssRewriter {
10
+ rewrite(query: string): string;
11
+ private rewriteKeywordWithParameter;
12
+ private problematicKeywords;
13
+ }
14
+ //#endregion
15
+ export { PssRewriter };
16
+ //# sourceMappingURL=pss-rewriter.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pss-rewriter.d.cts","names":[],"sources":["../../src/optimizer/pss-rewriter.ts"],"mappings":";;;;;AAKA;;;cAAa,WAAA;EACX,OAAA,CAAQ,KAAA;EAAA,QAIA,2BAAA;EAAA,QAaA,mBAAA;AAAA"}
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ //#region src/optimizer/pss-rewriter.d.ts
4
+ /**
5
+ * Rewriter for pg_stat_statements queries.
6
+ * Not all queries found in pg_stat_statements can be
7
+ * directly sent back to the database without first being rewritten.
8
+ */
9
+ declare class PssRewriter {
10
+ rewrite(query: string): string;
11
+ private rewriteKeywordWithParameter;
12
+ private problematicKeywords;
13
+ }
14
+ //#endregion
15
+ export { PssRewriter };
16
+ //# sourceMappingURL=pss-rewriter.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pss-rewriter.d.mts","names":[],"sources":["../../src/optimizer/pss-rewriter.ts"],"mappings":";;;;;AAKA;;;cAAa,WAAA;EACX,OAAA,CAAQ,KAAA;EAAA,QAIA,2BAAA;EAAA,QAaA,mBAAA;AAAA"}
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { _defineProperty } from "../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs";
3
+ //#region src/optimizer/pss-rewriter.ts
4
+ /**
5
+ * Rewriter for pg_stat_statements queries.
6
+ * Not all queries found in pg_stat_statements can be
7
+ * directly sent back to the database without first being rewritten.
8
+ */
9
+ var PssRewriter = class {
10
+ constructor() {
11
+ _defineProperty(this, "problematicKeywords", [
12
+ "interval",
13
+ "timestamp",
14
+ "geometry"
15
+ ]);
16
+ }
17
+ rewrite(query) {
18
+ return this.rewriteKeywordWithParameter(query);
19
+ }
20
+ rewriteKeywordWithParameter(query) {
21
+ return query.replace(/\b(\w+) (\$\d+)\b/gi, (match) => {
22
+ const [keyword, parameter] = match.split(" ");
23
+ if (!this.problematicKeywords.includes(keyword.toLowerCase())) return match;
24
+ return `(${parameter}::${keyword.toLowerCase()})`;
25
+ });
26
+ }
27
+ };
28
+ //#endregion
29
+ export { PssRewriter };
30
+
31
+ //# sourceMappingURL=pss-rewriter.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pss-rewriter.mjs","names":[],"sources":["../../src/optimizer/pss-rewriter.ts"],"sourcesContent":["/**\n * Rewriter for pg_stat_statements queries.\n * Not all queries found in pg_stat_statements can be\n * directly sent back to the database without first being rewritten.\n */\nexport class PssRewriter {\n rewrite(query: string): string {\n return this.rewriteKeywordWithParameter(query);\n }\n\n private rewriteKeywordWithParameter(query: string): string {\n return query.replace(/\\b(\\w+) (\\$\\d+)\\b/gi, (match) => {\n const [keyword, parameter] = match.split(\" \");\n const isProblematicKeyword = this.problematicKeywords.includes(\n keyword.toLowerCase(),\n );\n if (!isProblematicKeyword) {\n return match;\n }\n return `(${parameter}::${keyword.toLowerCase()})`;\n });\n }\n\n private problematicKeywords = [\"interval\", \"timestamp\", \"geometry\"];\n}\n"],"mappings":";;;;;;;;AAKA,IAAa,cAAb,MAAyB;;wBAkBf,uBAAsB;GAAC;GAAY;GAAa;GAAW,CAAC;;CAjBpE,QAAQ,OAAuB;AAC7B,SAAO,KAAK,4BAA4B,MAAM;;CAGhD,4BAAoC,OAAuB;AACzD,SAAO,MAAM,QAAQ,wBAAwB,UAAU;GACrD,MAAM,CAAC,SAAS,aAAa,MAAM,MAAM,IAAI;AAI7C,OAAI,CAHyB,KAAK,oBAAoB,SACpD,QAAQ,aAAa,CACtB,CAEC,QAAO;AAET,UAAO,IAAI,UAAU,IAAI,QAAQ,aAAa,CAAC;IAC/C"}