@query-doctor/core 0.8.0 → 0.8.1-rc.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.
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 -2550
  11. package/dist/index.d.cts +11 -776
  12. package/dist/index.d.mts +11 -776
  13. package/dist/index.mjs +11 -2506
  14. package/dist/optimizer/genalgo.cjs +352 -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 +349 -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 +311 -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 +310 -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
package/dist/index.cjs CHANGED
@@ -1,2552 +1,35 @@
1
1
  "use client";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- //#region \0rolldown/runtime.js
4
- var __create = Object.create;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
- var __getOwnPropNames = Object.getOwnPropertyNames;
8
- var __getProtoOf = Object.getPrototypeOf;
9
- var __hasOwnProp = Object.prototype.hasOwnProperty;
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
- key = keys[i];
13
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
- get: ((k) => from[k]).bind(null, key),
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
- });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
- value: mod,
22
- enumerable: true
23
- }) : target, mod));
24
- //#endregion
25
- let colorette = require("colorette");
26
- let pgsql_deparser = require("pgsql-deparser");
27
- let zod = require("zod");
28
- let dedent = require("dedent");
29
- dedent = __toESM(dedent);
30
- //#region src/sql/ast-utils.ts
31
- function is(node, kind) {
32
- return kind in node;
33
- }
34
- function isANode(node) {
35
- if (typeof node !== "object" || node === null) return false;
36
- const keys = Object.keys(node);
37
- return keys.length === 1 && /^[A-Z]/.test(keys[0]);
38
- }
39
- function getNodeKind(node) {
40
- return Object.keys(node)[0];
41
- }
42
- //#endregion
43
- //#region src/sql/nudges.ts
44
- const findFuncCallsOnColumns = (whereClause) => {
45
- const nudges = [];
46
- Walker.shallowMatch(whereClause, "FuncCall", (node) => {
47
- if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) nudges.push({
48
- kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
49
- severity: "WARNING",
50
- message: "Avoid using functions on columns in conditions — prevents index usage",
51
- location: node.FuncCall.location
52
- });
53
- });
54
- Walker.shallowMatch(whereClause, "CoalesceExpr", (node) => {
55
- if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) nudges.push({
56
- kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
57
- severity: "WARNING",
58
- message: "Avoid using functions on columns in conditions — prevents index usage",
59
- location: node.CoalesceExpr.location
60
- });
61
- });
62
- Walker.shallowMatch(whereClause, "TypeCast", (node) => {
63
- if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) nudges.push({
64
- kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
65
- severity: "WARNING",
66
- message: "Avoid using functions on columns in WHERE clause",
67
- location: node.TypeCast.location
68
- });
69
- });
70
- return nudges;
71
- };
72
- /**
73
- * Detect nudges for a single node during AST traversal.
74
- * Returns an array of nudges found for this node.
75
- */
76
- function parseNudges(node, stack) {
77
- const nudges = [];
78
- if (is(node, "SelectStmt")) {
79
- if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
80
- const star = node.SelectStmt.targetList?.find((target) => {
81
- if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) return false;
82
- const fields = target.ResTarget.val.ColumnRef.fields;
83
- if (!fields?.some((field) => is(field, "A_Star"))) return false;
84
- if (fields.length > 1) return false;
85
- return true;
86
- });
87
- if (star) {
88
- const fromClause = node.SelectStmt.fromClause;
89
- if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is(item, "RangeSubselect")))) nudges.push({
90
- kind: "AVOID_SELECT_STAR",
91
- severity: "INFO",
92
- message: "Avoid using SELECT *",
93
- location: star.ResTarget.location
94
- });
95
- }
96
- for (const target of node.SelectStmt.targetList ?? []) if (is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "SubLink") && target.ResTarget.val.SubLink.subLinkType === "EXPR_SUBLINK") nudges.push({
97
- kind: "AVOID_SCALAR_SUBQUERY_IN_SELECT",
98
- severity: "WARNING",
99
- message: "Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN",
100
- location: target.ResTarget.val.SubLink.location
101
- });
102
- }
103
- if (is(node, "SelectStmt")) {
104
- if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
105
- if (node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0) {
106
- if (node.SelectStmt.fromClause.some((fromItem) => {
107
- return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
108
- })) {
109
- const fromLocation = node.SelectStmt.fromClause.find((item) => is(item, "RangeVar"))?.RangeVar.location;
110
- if (!node.SelectStmt.whereClause) nudges.push({
111
- kind: "MISSING_WHERE_CLAUSE",
112
- severity: "INFO",
113
- message: "Missing WHERE clause",
114
- location: fromLocation
115
- });
116
- if (!node.SelectStmt.limitCount) nudges.push({
117
- kind: "MISSING_LIMIT_CLAUSE",
118
- severity: "INFO",
119
- message: "Missing LIMIT clause",
120
- location: fromLocation
121
- });
122
- }
123
- }
124
- }
125
- }
126
- if (is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
127
- if (!is(sortItem, "SortBy")) continue;
128
- const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
129
- const sortNulls = sortItem.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT";
130
- if (sortDir === "SORTBY_DESC" && sortNulls === "SORTBY_NULLS_DEFAULT") {
131
- if (sortItem.SortBy.node && is(sortItem.SortBy.node, "ColumnRef")) {
132
- const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);
133
- if (!(sortColumnName !== null && whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName))) nudges.push({
134
- kind: "NULLS_FIRST_IN_DESC_ORDER",
135
- severity: "INFO",
136
- message: "ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end",
137
- location: sortItem.SortBy.node.ColumnRef.location
138
- });
139
- }
140
- }
141
- }
142
- if (is(node, "A_Expr")) {
143
- if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>")) {
144
- const leftIsNull = isNullConstant(node.A_Expr.lexpr);
145
- const rightIsNull = isNullConstant(node.A_Expr.rexpr);
146
- if (leftIsNull || rightIsNull) nudges.push({
147
- kind: "USE_IS_NULL_NOT_EQUALS",
148
- severity: "WARNING",
149
- message: "Use IS NULL instead of = or != or <> for NULL comparisons",
150
- location: node.A_Expr.location
151
- });
152
- }
153
- if ((node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE") && node.A_Expr.rexpr) {
154
- const patternString = getStringConstantValue(node.A_Expr.rexpr);
155
- if (patternString && patternString.startsWith("%")) {
156
- let stringNode;
157
- if (is(node.A_Expr.rexpr, "A_Const")) stringNode = node.A_Expr.rexpr.A_Const;
158
- nudges.push({
159
- kind: "AVOID_LEADING_WILDCARD_LIKE",
160
- severity: "WARNING",
161
- message: "Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search",
162
- location: stringNode?.location
163
- });
164
- }
165
- }
166
- }
167
- if (is(node, "SelectStmt") && node.SelectStmt.sortClause) {
168
- for (const sortItem of node.SelectStmt.sortClause) if (is(sortItem, "SortBy") && sortItem.SortBy.node && is(sortItem.SortBy.node, "FuncCall") && sortItem.SortBy.node.FuncCall.funcname?.some((name) => is(name, "String") && name.String.sval === "random")) nudges.push({
169
- kind: "AVOID_ORDER_BY_RANDOM",
170
- severity: "WARNING",
171
- message: "Avoid using ORDER BY random()",
172
- location: sortItem.SortBy.node.FuncCall.location
173
- });
174
- }
175
- if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
176
- kind: "AVOID_DISTINCT_WITHOUT_REASON",
177
- severity: "WARNING",
178
- message: "Avoid using DISTINCT without a reason"
179
- });
180
- if (is(node, "JoinExpr")) {
181
- if (node.JoinExpr.quals) nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));
182
- else if (!node.JoinExpr.usingClause) nudges.push({
183
- kind: "MISSING_JOIN_CONDITION",
184
- severity: "WARNING",
185
- message: "Missing JOIN condition"
186
- });
187
- }
188
- if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
189
- const tables = node.SelectStmt.fromClause.filter((item) => is(item, "RangeVar"));
190
- if (tables.length > 1) {
191
- const tableNames = new Set(tables.map((t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || ""));
192
- if (!whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)) nudges.push({
193
- kind: "MISSING_JOIN_CONDITION",
194
- severity: "WARNING",
195
- message: "Missing JOIN condition",
196
- location: tables[1].RangeVar.location
197
- });
198
- }
199
- }
200
- if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
201
- if (countBoolOrConditions(node) >= 3 && allOrBranchesReferenceSameColumn(node)) nudges.push({
202
- kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
203
- severity: "WARNING",
204
- message: "Consider using IN instead of many ORs",
205
- location: node.BoolExpr.location
206
- });
207
- }
208
- if (is(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
209
- const args = node.BoolExpr.args;
210
- if (args && args.length === 1) {
211
- const inner = args[0];
212
- if (isANode(inner) && is(inner, "SubLink") && inner.SubLink.subLinkType === "ANY_SUBLINK") nudges.push({
213
- kind: "PREFER_NOT_EXISTS_OVER_NOT_IN",
214
- severity: "WARNING",
215
- message: "Prefer NOT EXISTS over NOT IN (SELECT ...)",
216
- location: inner.SubLink.location
217
- });
218
- }
219
- }
220
- if (is(node, "FuncCall")) {
221
- const funcName = node.FuncCall.funcname;
222
- if (funcName && funcName.length === 1 && is(funcName[0], "String") && funcName[0].String.sval === "count" && node.FuncCall.args && !node.FuncCall.agg_star && !node.FuncCall.agg_distinct) nudges.push({
223
- kind: "PREFER_COUNT_STAR_OVER_COUNT_COLUMN",
224
- severity: "INFO",
225
- message: "Prefer COUNT(*) over COUNT(column) or COUNT(1) — COUNT(*) counts rows without checking for NULLs. If you need to count non-NULL values, COUNT(column) is correct.",
226
- location: node.FuncCall.location
227
- });
228
- }
229
- if (is(node, "SelectStmt") && node.SelectStmt.havingClause) {
230
- if (!containsAggregate(node.SelectStmt.havingClause)) {
231
- const having = node.SelectStmt.havingClause;
232
- let location;
233
- if (is(having, "A_Expr")) location = having.A_Expr.location;
234
- else if (is(having, "BoolExpr")) location = having.BoolExpr.location;
235
- nudges.push({
236
- kind: "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES",
237
- severity: "INFO",
238
- message: "Non-aggregate condition in HAVING should be in WHERE",
239
- location
240
- });
241
- }
242
- }
243
- if (is(node, "FuncCall")) {
244
- if (stack.some((item) => item === "whereClause") && node.FuncCall.args) {
245
- const name = getFuncName(node);
246
- if (name && JSONB_SET_RETURNING_FUNCTIONS.has(name) && containsColumnRef(node.FuncCall.args)) nudges.push({
247
- kind: "CONSIDER_JSONB_CONTAINMENT_OPERATOR",
248
- severity: "INFO",
249
- message: "JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.",
250
- location: node.FuncCall.location
251
- });
252
- }
253
- }
254
- if (is(node, "A_Expr")) {
255
- if (node.A_Expr.kind === "AEXPR_IN") {
256
- let list;
257
- if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "List")) list = node.A_Expr.lexpr.List;
258
- else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "List")) list = node.A_Expr.rexpr.List;
259
- if (list?.items && list.items.length >= 10) nudges.push({
260
- kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
261
- message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
262
- severity: "INFO",
263
- location: node.A_Expr.location
264
- });
265
- }
266
- }
267
- if (is(node, "FuncCall")) {
268
- const funcname = node.FuncCall.funcname?.[0] && is(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
269
- if (funcname && [
270
- "sum",
271
- "count",
272
- "avg",
273
- "min",
274
- "max"
275
- ].includes(funcname.toLowerCase())) {
276
- const firstArg = node.FuncCall.args?.[0];
277
- if (firstArg && isANode(firstArg) && is(firstArg, "CaseExpr")) {
278
- const caseExpr = firstArg.CaseExpr;
279
- if (caseExpr.args && caseExpr.args.length === 1) {
280
- const defresult = caseExpr.defresult;
281
- if (!defresult || isANode(defresult) && is(defresult, "A_Const") && (defresult.A_Const.isnull !== void 0 || defresult.A_Const.ival !== void 0 && (defresult.A_Const.ival.ival === 0 || defresult.A_Const.ival.ival === void 0))) nudges.push({
282
- kind: "PREFER_FILTER_OVER_CASE_IN_AGGREGATE",
283
- severity: "INFO",
284
- message: "Use FILTER (WHERE ...) instead of CASE inside aggregate functions",
285
- location: node.FuncCall.location
286
- });
287
- }
288
- }
289
- }
290
- }
291
- if (is(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
292
- kind: "PREFER_UNION_ALL_OVER_UNION",
293
- severity: "INFO",
294
- message: "UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed"
295
- });
296
- if (is(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
297
- const opNode = node.A_Expr.name[0];
298
- const op = is(opNode, "String") ? opNode.String.sval : null;
299
- if (op && isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)) nudges.push({
300
- kind: "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK",
301
- severity: "INFO",
302
- message: "Use EXISTS instead of COUNT for existence checks",
303
- location: node.A_Expr.location
304
- });
305
- }
306
- return nudges;
307
- }
308
- function containsColumnRef(args) {
309
- for (const arg of args) if (hasColumnRefInNode(arg)) return true;
310
- return false;
311
- }
312
- function hasColumnRefInNode(node) {
313
- if (isANode(node) && is(node, "ColumnRef")) return true;
314
- if (typeof node !== "object" || node === null) return false;
315
- if (Array.isArray(node)) return node.some((item) => hasColumnRefInNode(item));
316
- if (isANode(node)) return hasColumnRefInNode(node[Object.keys(node)[0]]);
317
- for (const child of Object.values(node)) if (hasColumnRefInNode(child)) return true;
318
- return false;
319
- }
320
- function hasActualTablesInJoin(joinExpr) {
321
- if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) return true;
322
- if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
323
- if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) return true;
324
- }
325
- if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) return true;
326
- if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
327
- if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) return true;
328
- }
329
- return false;
330
- }
331
- function isNullConstant(node) {
332
- if (!node || typeof node !== "object") return false;
333
- if (isANode(node) && is(node, "A_Const")) return node.A_Const.isnull !== void 0;
334
- return false;
335
- }
336
- function getStringConstantValue(node) {
337
- if (!node || typeof node !== "object") return null;
338
- if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
339
- return null;
340
- }
341
- const JSONB_SET_RETURNING_FUNCTIONS = new Set([
342
- "jsonb_array_elements",
343
- "json_array_elements",
344
- "jsonb_array_elements_text",
345
- "json_array_elements_text",
346
- "jsonb_each",
347
- "json_each",
348
- "jsonb_each_text",
349
- "json_each_text"
350
- ]);
351
- function getFuncName(node) {
352
- const names = node.FuncCall.funcname;
353
- if (!names || names.length === 0) return null;
354
- const last = names[names.length - 1];
355
- if (isANode(last) && is(last, "String") && last.String.sval) return last.String.sval;
356
- return null;
357
- }
358
- function getLastColumnRefField(columnRef) {
359
- const fields = columnRef.ColumnRef.fields;
360
- if (!fields || fields.length === 0) return null;
361
- const lastField = fields[fields.length - 1];
362
- if (isANode(lastField) && is(lastField, "String")) return lastField.String.sval || null;
363
- return null;
364
- }
365
- function whereHasIsNotNull(whereClause, columnName) {
366
- if (!whereClause) return false;
367
- let found = false;
368
- Walker.shallowMatch(whereClause, "NullTest", (node) => {
369
- if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is(node.NullTest.arg, "ColumnRef")) {
370
- if (getLastColumnRefField(node.NullTest.arg) === columnName) found = true;
371
- }
372
- });
373
- return found;
374
- }
375
- const AGGREGATE_FUNCTIONS = new Set([
376
- "count",
377
- "sum",
378
- "avg",
379
- "min",
380
- "max",
381
- "array_agg",
382
- "string_agg",
383
- "bool_and",
384
- "bool_or",
385
- "every"
386
- ]);
387
- function containsAggregate(node) {
388
- if (!node || typeof node !== "object") return false;
389
- if (Array.isArray(node)) return node.some(containsAggregate);
390
- if (isANode(node) && is(node, "FuncCall")) {
391
- const funcname = node.FuncCall.funcname;
392
- if (funcname) {
393
- for (const f of funcname) if (isANode(f) && is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
394
- }
395
- }
396
- if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
397
- for (const child of Object.values(node)) if (containsAggregate(child)) return true;
398
- return false;
399
- }
400
- function countBoolOrConditions(node) {
401
- if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
402
- let count = 0;
403
- for (const arg of node.BoolExpr.args) if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") count += countBoolOrConditions(arg);
404
- else count += 1;
405
- return count;
406
- }
407
- /**
408
- * Check whether every leaf of a top-level OR expression references the same
409
- * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).
410
- * Returns false when ORs span different columns — IN rewrite doesn't apply.
411
- */
412
- function allOrBranchesReferenceSameColumn(node) {
413
- const columns = collectOrLeafColumns(node);
414
- if (columns.length === 0) return false;
415
- return columns.every((col) => col === columns[0]);
416
- }
417
- function collectOrLeafColumns(node) {
418
- if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return [];
419
- const columns = [];
420
- for (const arg of node.BoolExpr.args) {
421
- if (!isANode(arg)) continue;
422
- if (is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") columns.push(...collectOrLeafColumns(arg));
423
- else if (is(arg, "A_Expr")) {
424
- const col = getLeftColumnKey(arg);
425
- if (col) columns.push(col);
426
- else return [];
427
- } else return [];
428
- }
429
- return columns;
430
- }
431
- /**
432
- * Get a string key for the left-hand column of an A_Expr equality comparison.
433
- * For `t1.col = value` returns `"t1.col"`, for `col = value` returns `"col"`.
434
- */
435
- function getLeftColumnKey(expr) {
436
- if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;
437
- if (!is(expr.A_Expr.lexpr, "ColumnRef")) return null;
438
- const fields = expr.A_Expr.lexpr.ColumnRef.fields;
439
- if (!fields) return null;
440
- return fields.filter((f) => isANode(f) && is(f, "String")).map((f) => f.String.sval).join(".");
441
- }
442
- /**
443
- * Check if a WHERE clause contains an equality between columns from different
444
- * tables (indicating an old-style implicit join condition).
445
- */
446
- function whereHasCrossTableEquality(whereClause, tableNames) {
447
- if (!whereClause) return false;
448
- if (isANode(whereClause) && is(whereClause, "A_Expr")) {
449
- if (whereClause.A_Expr.kind === "AEXPR_OP" && whereClause.A_Expr.name?.some((n) => is(n, "String") && n.String.sval === "=")) {
450
- const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);
451
- const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);
452
- if (leftTable && rightTable && leftTable !== rightTable && tableNames.has(leftTable) && tableNames.has(rightTable)) return true;
453
- }
454
- }
455
- if (isANode(whereClause) && is(whereClause, "BoolExpr")) {
456
- for (const arg of whereClause.BoolExpr.args ?? []) if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) return true;
457
- }
458
- return false;
459
- }
460
- /**
461
- * Extract the table qualifier (first field) from a ColumnRef node.
462
- * e.g. `t1.uid` → `"t1"`, `uid` → null
463
- */
464
- function getColumnRefTableQualifier(node) {
465
- if (!node || !isANode(node) || !is(node, "ColumnRef")) return null;
466
- const fields = node.ColumnRef.fields;
467
- if (!fields || fields.length < 2) return null;
468
- const first = fields[0];
469
- if (isANode(first) && is(first, "String")) return first.String.sval || null;
470
- return null;
471
- }
472
- function isCountFuncCall(node) {
473
- if (!node || typeof node !== "object") return false;
474
- if (!isANode(node) || !is(node, "FuncCall")) return false;
475
- const fc = node.FuncCall;
476
- if (!(fc.funcname?.some((n) => is(n, "String") && n.String.sval === "count") ?? false)) return false;
477
- if (fc.agg_star) return true;
478
- if (fc.args && fc.args.length === 1 && isANode(fc.args[0]) && is(fc.args[0], "A_Const")) return true;
479
- return false;
480
- }
481
- function isSubLinkWithCount(node) {
482
- if (!node || typeof node !== "object") return false;
483
- if (!isANode(node) || !is(node, "SubLink")) return false;
484
- const subselect = node.SubLink.subselect;
485
- if (!subselect || !isANode(subselect) || !is(subselect, "SelectStmt")) return false;
486
- const targets = subselect.SelectStmt.targetList;
487
- if (!targets || targets.length !== 1) return false;
488
- const target = targets[0];
489
- if (!isANode(target) || !is(target, "ResTarget") || !target.ResTarget.val) return false;
490
- return isCountFuncCall(target.ResTarget.val);
491
- }
492
- function isCountExpression(node) {
493
- return isCountFuncCall(node) || isSubLinkWithCount(node);
494
- }
495
- function getIntegerConstantValue(node) {
496
- if (!node || typeof node !== "object") return null;
497
- if (!isANode(node) || !is(node, "A_Const")) return null;
498
- if (node.A_Const.ival === void 0) return null;
499
- return node.A_Const.ival.ival ?? 0;
500
- }
501
- function isExistenceCheckPattern(lexpr, rexpr, op) {
502
- if (isCountExpression(lexpr)) {
503
- const val = getIntegerConstantValue(rexpr);
504
- if (val !== null) {
505
- if (op === ">" && val === 0) return true;
506
- if (op === ">=" && val === 1) return true;
507
- if ((op === "!=" || op === "<>") && val === 0) return true;
508
- }
509
- }
510
- if (isCountExpression(rexpr)) {
511
- const val = getIntegerConstantValue(lexpr);
512
- if (val !== null) {
513
- if (op === "<" && val === 0) return true;
514
- if (op === "<=" && val === 1) return true;
515
- if ((op === "!=" || op === "<>") && val === 0) return true;
516
- }
517
- }
518
- return false;
519
- }
520
- //#endregion
521
- //#region \0@oxc-project+runtime@0.122.0/helpers/typeof.js
522
- function _typeof(o) {
523
- "@babel/helpers - typeof";
524
- return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
525
- return typeof o;
526
- } : function(o) {
527
- return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
528
- }, _typeof(o);
529
- }
530
- //#endregion
531
- //#region \0@oxc-project+runtime@0.122.0/helpers/toPrimitive.js
532
- function toPrimitive(t, r) {
533
- if ("object" != _typeof(t) || !t) return t;
534
- var e = t[Symbol.toPrimitive];
535
- if (void 0 !== e) {
536
- var i = e.call(t, r || "default");
537
- if ("object" != _typeof(i)) return i;
538
- throw new TypeError("@@toPrimitive must return a primitive value.");
539
- }
540
- return ("string" === r ? String : Number)(t);
541
- }
542
- //#endregion
543
- //#region \0@oxc-project+runtime@0.122.0/helpers/toPropertyKey.js
544
- function toPropertyKey(t) {
545
- var i = toPrimitive(t, "string");
546
- return "symbol" == _typeof(i) ? i : i + "";
547
- }
548
- //#endregion
549
- //#region \0@oxc-project+runtime@0.122.0/helpers/defineProperty.js
550
- function _defineProperty(e, r, t) {
551
- return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
552
- value: t,
553
- enumerable: !0,
554
- configurable: !0,
555
- writable: !0
556
- }) : e[r] = t, e;
557
- }
558
- //#endregion
559
- //#region src/sql/walker.ts
560
- const JSONB_EXTRACTION_OPS = new Set(["->", "->>"]);
561
- const COMPARISON_OPS = new Set([
562
- "=",
563
- "<>",
564
- "!=",
565
- "<",
566
- "<=",
567
- ">",
568
- ">=",
569
- "~~",
570
- "~~*",
571
- "!~~",
572
- "!~~*"
573
- ]);
574
- /**
575
- * Walks the AST of a sql query and extracts query metadata.
576
- * This pattern is used to segregate the mutable state that's more common for the
577
- * AST walking process from the rest of the analyzer.
578
- */
579
- var Walker = class Walker {
580
- constructor(query) {
581
- this.query = query;
582
- _defineProperty(this, "tableMappings", /* @__PURE__ */ new Map());
583
- _defineProperty(this, "tempTables", /* @__PURE__ */ new Set());
584
- _defineProperty(this, "highlights", []);
585
- _defineProperty(this, "indexRepresentations", /* @__PURE__ */ new Set());
586
- _defineProperty(this, "indexesToCheck", []);
587
- _defineProperty(this, "highlightPositions", /* @__PURE__ */ new Set());
588
- _defineProperty(this, "seenReferences", /* @__PURE__ */ new Map());
589
- _defineProperty(this, "shadowedAliases", []);
590
- _defineProperty(this, "nudges", []);
591
- }
592
- walk(root) {
593
- this.tableMappings = /* @__PURE__ */ new Map();
594
- this.tempTables = /* @__PURE__ */ new Set();
595
- this.highlights = [];
596
- this.indexRepresentations = /* @__PURE__ */ new Set();
597
- this.indexesToCheck = [];
598
- this.highlightPositions = /* @__PURE__ */ new Set();
599
- this.seenReferences = /* @__PURE__ */ new Map();
600
- this.shadowedAliases = [];
601
- this.nudges = [];
602
- Walker.traverse(root, (node, stack) => {
603
- const nodeNudges = parseNudges(node, stack);
604
- this.nudges = [...this.nudges, ...nodeNudges];
605
- if (is(node, "CommonTableExpr")) {
606
- if (node.CommonTableExpr.ctename) this.tempTables.add(node.CommonTableExpr.ctename);
607
- }
608
- if (is(node, "RangeSubselect")) {
609
- if (node.RangeSubselect.alias?.aliasname) this.tempTables.add(node.RangeSubselect.alias.aliasname);
610
- }
611
- if (is(node, "NullTest")) {
612
- if (node.NullTest.arg && node.NullTest.nulltesttype && is(node.NullTest.arg, "ColumnRef")) this.add(node.NullTest.arg, { where: { nulltest: node.NullTest.nulltesttype } });
613
- }
614
- if (is(node, "RangeVar") && node.RangeVar.relname) {
615
- const columnReference = {
616
- text: node.RangeVar.relname,
617
- start: node.RangeVar.location,
618
- quoted: false
619
- };
620
- if (node.RangeVar.schemaname) columnReference.schema = node.RangeVar.schemaname;
621
- this.tableMappings.set(node.RangeVar.relname, columnReference);
622
- if (node.RangeVar.alias?.aliasname) {
623
- const aliasName = node.RangeVar.alias.aliasname;
624
- const existingMapping = this.tableMappings.get(aliasName);
625
- const part = {
626
- text: node.RangeVar.relname,
627
- start: node.RangeVar.location,
628
- quoted: true,
629
- alias: aliasName
630
- };
631
- if (node.RangeVar.schemaname) part.schema = node.RangeVar.schemaname;
632
- if (existingMapping) {
633
- if (!(node.RangeVar.relname?.startsWith("pg_") ?? false)) console.warn(`Ignoring alias ${aliasName} as it shadows an existing mapping for ${existingMapping.text}. We currently do not support alias shadowing.`);
634
- this.shadowedAliases.push(part);
635
- return;
636
- }
637
- this.tableMappings.set(aliasName, part);
638
- }
639
- }
640
- if (is(node, "SortBy")) {
641
- if (node.SortBy.node && is(node.SortBy.node, "ColumnRef")) this.add(node.SortBy.node, { sort: {
642
- dir: node.SortBy.sortby_dir ?? "SORTBY_DEFAULT",
643
- nulls: node.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT"
644
- } });
645
- }
646
- if (is(node, "JoinExpr") && node.JoinExpr.quals) {
647
- if (is(node.JoinExpr.quals, "A_Expr")) {
648
- if (node.JoinExpr.quals.A_Expr.lexpr && is(node.JoinExpr.quals.A_Expr.lexpr, "ColumnRef")) this.add(node.JoinExpr.quals.A_Expr.lexpr);
649
- if (node.JoinExpr.quals.A_Expr.rexpr && is(node.JoinExpr.quals.A_Expr.rexpr, "ColumnRef")) this.add(node.JoinExpr.quals.A_Expr.rexpr);
650
- }
651
- }
652
- if (is(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
653
- const opName = node.A_Expr.name?.[0] && is(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
654
- if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&" || opName === "@@" || opName === "@?")) {
655
- const jsonbOperator = opName;
656
- if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "ColumnRef")) this.add(node.A_Expr.lexpr, { jsonbOperator });
657
- if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "ColumnRef")) this.add(node.A_Expr.rexpr, { jsonbOperator });
658
- }
659
- if (opName && COMPARISON_OPS.has(opName)) for (const operand of [node.A_Expr.lexpr, node.A_Expr.rexpr]) {
660
- if (!operand) continue;
661
- const extraction = extractJsonbPath(operand);
662
- if (extraction) this.add(extraction.columnRef, { jsonbExtraction: extraction.expression });
663
- }
664
- }
665
- if (is(node, "ColumnRef")) {
666
- for (let i = 0; i < stack.length; i++) {
667
- if (stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef") {
668
- this.add(node, { ignored: true });
669
- return;
670
- }
671
- if (stack[i + 1] === "targetList" && stack[i + 2] === "ResTarget" && stack[i + 3] === "val" && stack[i + 4] === "ColumnRef") {
672
- this.add(node, { ignored: true });
673
- return;
674
- }
675
- if (stack[i] === "FuncCall" && stack[i + 1] === "args") {
676
- this.add(node, { ignored: true });
677
- return;
678
- }
679
- }
680
- this.add(node);
681
- }
682
- });
683
- return {
684
- highlights: this.highlights,
685
- indexRepresentations: this.indexRepresentations,
686
- indexesToCheck: this.indexesToCheck,
687
- shadowedAliases: this.shadowedAliases,
688
- tempTables: this.tempTables,
689
- tableMappings: this.tableMappings,
690
- nudges: this.nudges
691
- };
692
- }
693
- add(node, options) {
694
- if (!node.ColumnRef.location) {
695
- console.error(`Node did not have a location. Skipping`, node);
696
- return;
697
- }
698
- if (!node.ColumnRef.fields) {
699
- console.error(node);
700
- throw new Error("Column reference must have fields");
701
- }
702
- let ignored = options?.ignored ?? false;
703
- let runningLength = node.ColumnRef.location;
704
- const parts = node.ColumnRef.fields.map((field, i, length) => {
705
- if (!is(field, "String") || !field.String.sval) {
706
- const out = (0, pgsql_deparser.deparseSync)(field);
707
- ignored = true;
708
- return {
709
- quoted: out.startsWith("\""),
710
- text: out,
711
- start: runningLength
712
- };
713
- }
714
- const start = runningLength;
715
- const size = field.String.sval?.length ?? 0;
716
- let quoted = false;
717
- if (node.ColumnRef.location !== void 0) {
718
- if (this.query[runningLength] === "\"") quoted = true;
719
- }
720
- const isLastIteration = i === length.length - 1;
721
- runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);
722
- return {
723
- text: field.String.sval,
724
- start,
725
- quoted
726
- };
727
- });
728
- const end = runningLength;
729
- if (this.highlightPositions.has(node.ColumnRef.location)) return;
730
- this.highlightPositions.add(node.ColumnRef.location);
731
- const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;
732
- const seen = this.seenReferences.get(highlighted);
733
- if (!ignored) this.seenReferences.set(highlighted, (seen ?? 0) + 1);
734
- const ref = {
735
- frequency: seen ?? 1,
736
- representation: highlighted,
737
- parts,
738
- ignored: ignored ?? false,
739
- position: {
740
- start: node.ColumnRef.location,
741
- end
742
- }
743
- };
744
- if (options?.sort) ref.sort = options.sort;
745
- if (options?.where) ref.where = options.where;
746
- if (options?.jsonbOperator) ref.jsonbOperator = options.jsonbOperator;
747
- if (options?.jsonbExtraction) ref.jsonbExtraction = options.jsonbExtraction;
748
- this.highlights.push(ref);
749
- }
750
- /**
751
- * Descend only into shallow combinators of a node such as
752
- * - And
753
- * - Or
754
- * - Not
755
- * - ::typecast
756
- * without deep traversing into subqueries. Useful for checking members
757
- * of a `WHERE` clause
758
- */
759
- static shallowMatch(expr, kind, callback) {
760
- if (is(expr, kind)) {
761
- callback(expr);
762
- return;
763
- }
764
- if (is(expr, "BoolExpr") && expr.BoolExpr.args) {
765
- for (const arg of expr.BoolExpr.args) Walker.shallowMatch(arg, kind, callback);
766
- return;
767
- }
768
- if (is(expr, "A_Expr")) {
769
- if (expr.A_Expr.lexpr) Walker.shallowMatch(expr.A_Expr.lexpr, kind, callback);
770
- if (expr.A_Expr.rexpr) Walker.shallowMatch(expr.A_Expr.rexpr, kind, callback);
771
- return;
772
- }
773
- if (is(expr, "NullTest") && expr.NullTest.arg) {
774
- Walker.shallowMatch(expr.NullTest.arg, kind, callback);
775
- return;
776
- }
777
- if (is(expr, "BooleanTest") && expr.BooleanTest.arg) {
778
- Walker.shallowMatch(expr.BooleanTest.arg, kind, callback);
779
- return;
780
- }
781
- if (is(expr, "SubLink") && expr.SubLink.testexpr) {
782
- Walker.shallowMatch(expr.SubLink.testexpr, kind, callback);
783
- return;
784
- }
785
- if (is(expr, "TypeCast") && expr.TypeCast.arg) {
786
- Walker.shallowMatch(expr.TypeCast.arg, kind, callback);
787
- return;
788
- }
789
- }
790
- static traverse(node, callback) {
791
- Walker.doTraverse(node, [], callback);
792
- }
793
- static doTraverse(node, stack, callback) {
794
- if (isANode(node)) callback(node, [...stack, getNodeKind(node)]);
795
- if (typeof node !== "object" || node === null) return;
796
- if (Array.isArray(node)) {
797
- for (const item of node) if (isANode(item)) Walker.doTraverse(item, stack, callback);
798
- } else if (isANode(node)) {
799
- const keys = Object.keys(node);
800
- Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
801
- } else for (const [key, child] of Object.entries(node)) Walker.doTraverse(child, [...stack, key], callback);
802
- }
803
- };
804
- /**
805
- * Given an operand of a comparison (e.g. the left side of `=`), check whether
806
- * it is a JSONB path extraction expression such as `data->>'email'` or
807
- * `(data->>'age')::int`. If so, return the root ColumnRef (for the walker to
808
- * register) and the full expression string (for use in the expression index).
809
- *
810
- * Handles:
811
- * - `data->>'email'` — simple extraction
812
- * - `(data->>'age')::int` — extraction with cast
813
- * - `data->'addr'->>'city'` — chained extraction
814
- * - `t.data->>'email'` — table-qualified (strips qualifier from expression)
815
- */
816
- function extractJsonbPath(node) {
817
- let exprNode = node;
818
- if (is(exprNode, "TypeCast") && exprNode.TypeCast.arg) exprNode = exprNode.TypeCast.arg;
819
- if (!is(exprNode, "A_Expr") || exprNode.A_Expr.kind !== "AEXPR_OP") return;
820
- const innerOp = exprNode.A_Expr.name?.[0] && is(exprNode.A_Expr.name[0], "String") && exprNode.A_Expr.name[0].String.sval;
821
- if (!innerOp || !JSONB_EXTRACTION_OPS.has(innerOp)) return;
822
- const rootCol = findRootColumnRef(exprNode);
823
- if (!rootCol) return;
824
- const cloned = JSON.parse(JSON.stringify(node));
825
- stripTableQualifiers(cloned);
826
- return {
827
- columnRef: rootCol,
828
- expression: (0, pgsql_deparser.deparseSync)(cloned)
829
- };
830
- }
831
- /**
832
- * Walk the left side of a chain of `->` / `->>` operators to find the
833
- * root ColumnRef (the JSONB column itself).
834
- */
835
- function findRootColumnRef(node) {
836
- if (is(node, "ColumnRef")) return node;
837
- if (is(node, "A_Expr") && node.A_Expr.lexpr) return findRootColumnRef(node.A_Expr.lexpr);
838
- if (is(node, "TypeCast") && node.TypeCast.arg) return findRootColumnRef(node.TypeCast.arg);
839
- }
840
- /**
841
- * Remove table/schema qualifiers from ColumnRef nodes inside the
842
- * expression so the deparsed expression contains only the column name.
843
- * e.g. `t.data->>'email'` → `data->>'email'`
844
- */
845
- function stripTableQualifiers(node) {
846
- if (typeof node !== "object" || node === null) return;
847
- if ("ColumnRef" in node && node.ColumnRef?.fields) {
848
- const fields = node.ColumnRef.fields;
849
- if (fields.length > 1) node.ColumnRef.fields = [fields[fields.length - 1]];
850
- return;
851
- }
852
- for (const value of Object.values(node)) if (Array.isArray(value)) for (const item of value) stripTableQualifiers(item);
853
- else if (typeof value === "object" && value !== null) stripTableQualifiers(value);
854
- }
855
- //#endregion
856
- //#region src/sql/analyzer.ts
857
- const ignoredIdentifier = "__qd_placeholder";
858
- /**
859
- * Analyzes a query and returns a list of column references that
860
- * should be indexed.
861
- *
862
- * This should be instantiated once per analyzed query.
863
- */
864
- var Analyzer = class {
865
- constructor(parser) {
866
- this.parser = parser;
867
- }
868
- async analyze(query, formattedQuery) {
869
- const ast = await this.parser(query);
870
- if (!ast.stmts) throw new Error("Query did not have any statements. This should probably never happen?");
871
- const stmt = ast.stmts[0].stmt;
872
- if (!stmt) throw new Error("Query did not have any statements. This should probably never happen?");
873
- const { highlights, indexRepresentations, indexesToCheck, shadowedAliases, tempTables, tableMappings, nudges } = new Walker(query).walk(stmt);
874
- const sortedHighlights = highlights.sort((a, b) => b.position.end - a.position.end);
875
- let currQuery = query;
876
- for (const highlight of sortedHighlights) {
877
- const parts = this.resolveTableAliases(highlight.parts, tableMappings);
878
- if (parts.length === 0) {
879
- console.error(highlight);
880
- throw new Error("Highlight must have at least one part");
881
- }
882
- let color;
883
- let skip = false;
884
- if (highlight.ignored) {
885
- color = (x) => (0, colorette.dim)((0, colorette.strikethrough)(x));
886
- skip = true;
887
- } else if (parts.length === 2 && tempTables.has(parts[0].text) && !tableMappings.has(parts[0].text)) {
888
- color = colorette.blue;
889
- skip = true;
890
- } else color = colorette.bgMagentaBright;
891
- const queryRepr = highlight.representation;
892
- const queryBeforeMatch = currQuery.slice(0, highlight.position.start);
893
- const queryAfterToken = currQuery.slice(highlight.position.end);
894
- currQuery = `${queryBeforeMatch}${color(queryRepr)}${this.colorizeKeywords(queryAfterToken, color)}`;
895
- const reprKey = highlight.jsonbExtraction ? `${queryRepr}::${highlight.jsonbExtraction}` : queryRepr;
896
- if (indexRepresentations.has(reprKey)) skip = true;
897
- if (!skip) {
898
- indexesToCheck.push(highlight);
899
- indexRepresentations.add(reprKey);
900
- }
901
- }
902
- const referencedTables = [];
903
- for (const value of tableMappings.values()) if (!value.alias) referencedTables.push({
904
- schema: value.schema,
905
- table: value.text
906
- });
907
- const { tags, queryWithoutTags } = this.extractSqlcommenter(query);
908
- const formattedQueryWithoutTags = formattedQuery ? this.extractSqlcommenter(formattedQuery).queryWithoutTags : void 0;
909
- return {
910
- indexesToCheck,
911
- ansiHighlightedQuery: currQuery,
912
- referencedTables,
913
- shadowedAliases,
914
- tags,
915
- queryWithoutTags,
916
- formattedQueryWithoutTags,
917
- nudges
918
- };
919
- }
920
- deriveIndexes(tables, discovered, referencedTables) {
921
- /**
922
- * There are 3 different kinds of parts a col reference can have
923
- * {a} = just a column within context. Find out the table
924
- * {a, b} = a column reference with a table reference. There's still ambiguity here
925
- * with what the schema could be in case there are 2 tables with the same name in different schemas.
926
- * {a, b, c} = a column reference with a table reference and a schema reference.
927
- * This is the best case scenario.
928
- */
929
- const allIndexes = [];
930
- const seenIndexes = /* @__PURE__ */ new Set();
931
- function addIndex(index) {
932
- const extractionSuffix = index.jsonbExtraction ? `:"${index.jsonbExtraction}"` : "";
933
- const key = `"${index.schema}":"${index.table}":"${index.column}"${extractionSuffix}`;
934
- if (seenIndexes.has(key)) return;
935
- seenIndexes.add(key);
936
- allIndexes.push(index);
937
- }
938
- const matchingTables = this.filterReferences(referencedTables, tables);
939
- for (const colReference of discovered) {
940
- const partsCount = colReference.parts.length;
941
- const columnOnlyReference = partsCount === 1;
942
- const tableReference = partsCount === 2;
943
- const fullReference = partsCount === 3;
944
- if (columnOnlyReference) {
945
- const [column] = colReference.parts;
946
- const referencedColumn = this.normalize(column);
947
- for (const table of matchingTables) {
948
- if (!this.hasColumn(table, referencedColumn)) continue;
949
- const index = {
950
- schema: table.schemaName,
951
- table: table.tableName,
952
- column: referencedColumn
953
- };
954
- if (colReference.sort) index.sort = colReference.sort;
955
- if (colReference.where) index.where = colReference.where;
956
- if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
957
- if (colReference.jsonbExtraction) index.jsonbExtraction = colReference.jsonbExtraction;
958
- addIndex(index);
959
- }
960
- } else if (tableReference) {
961
- const [table, column] = colReference.parts;
962
- const referencedTable = this.normalize(table);
963
- const referencedColumn = this.normalize(column);
964
- for (const matchingTable of matchingTables) {
965
- if (!this.hasColumn(matchingTable, referencedColumn)) continue;
966
- const index = {
967
- schema: matchingTable.schemaName,
968
- table: referencedTable,
969
- column: referencedColumn
970
- };
971
- if (colReference.sort) index.sort = colReference.sort;
972
- if (colReference.where) index.where = colReference.where;
973
- if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
974
- if (colReference.jsonbExtraction) index.jsonbExtraction = colReference.jsonbExtraction;
975
- addIndex(index);
976
- }
977
- } else if (fullReference) {
978
- const [schema, table, column] = colReference.parts;
979
- const index = {
980
- schema: this.normalize(schema),
981
- table: this.normalize(table),
982
- column: this.normalize(column)
983
- };
984
- if (colReference.sort) index.sort = colReference.sort;
985
- if (colReference.where) index.where = colReference.where;
986
- if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
987
- if (colReference.jsonbExtraction) index.jsonbExtraction = colReference.jsonbExtraction;
988
- addIndex(index);
989
- } else {
990
- console.error("Column reference has too many parts. The query is malformed", colReference);
991
- continue;
992
- }
993
- }
994
- return allIndexes;
995
- }
996
- filterReferences(referencedTables, tables) {
997
- const matchingTables = [];
998
- for (const referencedTable of referencedTables) {
999
- const refs = tables.filter(({ tableName, schemaName }) => {
1000
- let schemaMatches = true;
1001
- if (referencedTable.schema) schemaMatches = schemaName === referencedTable.schema;
1002
- return schemaMatches && tableName === referencedTable.table;
1003
- });
1004
- matchingTables.push(...refs);
1005
- }
1006
- return matchingTables;
1007
- }
1008
- hasColumn(table, columnName) {
1009
- return table.columns?.some((column) => column.columnName === columnName) ?? false;
1010
- }
1011
- colorizeKeywords(query, color) {
1012
- return query.replace(/(^\s+)(asc|desc)?(\s+(nulls first|nulls last))?/i, (_, pre, dir, spaceNulls, nulls) => {
1013
- return `${pre}${dir ? color(dir) : ""}${nulls ? spaceNulls.replace(nulls, color(nulls)) : ""}`;
1014
- }).replace(/(^\s+)(is (null|not null))/i, (_, pre, nulltest) => {
1015
- return `${pre}${color(nulltest)}`;
1016
- });
1017
- }
1018
- /**
1019
- * Resolves aliases such as `a.b` to `x.b` if `a` is a known
1020
- * alias to a table called x.
1021
- *
1022
- * Ignores all other combination of parts such as `a.b.c`
1023
- */
1024
- resolveTableAliases(parts, tableMappings) {
1025
- if (parts.length !== 2) return parts;
1026
- const tablePart = parts[0];
1027
- const mapping = tableMappings.get(tablePart.text);
1028
- if (mapping) parts[0] = mapping;
1029
- return parts;
1030
- }
1031
- normalize(columnReference) {
1032
- return columnReference.quoted ? columnReference.text : columnReference.text.toLowerCase();
1033
- }
1034
- extractSqlcommenter(query) {
1035
- const trimmedQuery = query.trimEnd();
1036
- const startPosition = trimmedQuery.lastIndexOf("/*");
1037
- const endPosition = trimmedQuery.lastIndexOf("*/");
1038
- if (startPosition === -1 || endPosition === -1) return {
1039
- tags: [],
1040
- queryWithoutTags: trimmedQuery
1041
- };
1042
- const afterComment = trimmedQuery.slice(endPosition + 2).trim();
1043
- if (afterComment && afterComment !== ";") return {
1044
- tags: [],
1045
- queryWithoutTags: trimmedQuery
1046
- };
1047
- const queryWithoutTags = trimmedQuery.slice(0, startPosition);
1048
- const tagString = trimmedQuery.slice(startPosition + 2, endPosition).trim();
1049
- if (!tagString || typeof tagString !== "string") return {
1050
- tags: [],
1051
- queryWithoutTags
1052
- };
1053
- const tags = [];
1054
- for (const match of tagString.split(",")) {
1055
- const [key, value] = match.split("=");
1056
- if (!key || !value) {
1057
- if (tags.length > 0) console.warn(`Invalid sqlcommenter tag: ${match} in comment: ${tagString}. Ignoring`);
1058
- continue;
1059
- }
1060
- try {
1061
- let sliceStart = 0;
1062
- if (value.startsWith("'")) sliceStart = 1;
1063
- let sliceEnd = value.length;
1064
- if (value.endsWith("'")) sliceEnd -= 1;
1065
- const decoded = decodeURIComponent(value.slice(sliceStart, sliceEnd));
1066
- tags.push({
1067
- key: key.trim(),
1068
- value: decoded
1069
- });
1070
- } catch (err) {
1071
- console.error(err);
1072
- }
1073
- }
1074
- return {
1075
- tags,
1076
- queryWithoutTags
1077
- };
1078
- }
1079
- };
1080
- //#endregion
1081
- //#region src/sql/database.ts
1082
- const PostgresVersion = zod.z.string().brand("PostgresVersion");
1083
- /** Zod schema for PostgresExplainStage — validates common fields, passes through variant-specific ones. */
1084
- const PostgresExplainStageSchema = zod.z.lazy(() => zod.z.object({
1085
- "Node Type": zod.z.string(),
1086
- "Total Cost": zod.z.number(),
1087
- "Plan Width": zod.z.number(),
1088
- "Node Id": zod.z.number().optional(),
1089
- Plans: zod.z.array(PostgresExplainStageSchema).optional()
1090
- }).passthrough());
1091
- /**
1092
- * Drops a disabled index. Rollsback if it fails for any reason
1093
- * @returns Did dropping the index succeed?
1094
- */
1095
- async function dropIndex(tx, index) {
1096
- try {
1097
- await tx.exec(`
1098
- savepoint idx_drop;
1099
- drop index if exists ${index} cascade;
1100
- `);
1101
- return true;
1102
- } catch {
1103
- await tx.exec(`rollback to idx_drop`);
1104
- return false;
1105
- }
1106
- }
1107
- //#endregion
1108
- //#region src/sql/indexes.ts
1109
- function isIndexSupported(index) {
1110
- return index.index_type === "btree" || index.index_type === "gin";
1111
- }
1112
- /**
1113
- * Doesn't necessarily decide whether the index can be dropped but can be
1114
- * used to not even try dropping indexes that _definitely_ cannot be dropped
1115
- */
1116
- function isIndexProbablyDroppable(index) {
1117
- return !index.is_primary && !index.is_unique;
1118
- }
1119
- //#endregion
1120
- //#region src/sql/builder.ts
1121
- var PostgresQueryBuilder = class PostgresQueryBuilder {
1122
- constructor(query) {
1123
- this.query = query;
1124
- _defineProperty(this, "commands", {});
1125
- _defineProperty(this, "isIntrospection", false);
1126
- _defineProperty(this, "explainFlags", []);
1127
- _defineProperty(this, "_preamble", 0);
1128
- _defineProperty(this, "parameters", {});
1129
- _defineProperty(this, "limitSubstitution", void 0);
1130
- }
1131
- get preamble() {
1132
- return this._preamble;
1133
- }
1134
- static createIndex(definition, name) {
1135
- if (name) return new PostgresQueryBuilder(`create index "${name}" on ${definition};`);
1136
- return new PostgresQueryBuilder(`create index on ${definition};`);
1137
- }
1138
- enable(command, value = true) {
1139
- const commandString = `enable_${command}`;
1140
- if (value) this.commands[commandString] = "on";
1141
- else this.commands[commandString] = "off";
1142
- return this;
1143
- }
1144
- withQuery(query) {
1145
- this.query = query;
1146
- return this;
1147
- }
1148
- introspect() {
1149
- this.isIntrospection = true;
1150
- return this;
1151
- }
1152
- explain(flags) {
1153
- this.explainFlags = flags;
1154
- return this;
1155
- }
1156
- parameterize(parameters) {
1157
- Object.assign(this.parameters, parameters);
1158
- return this;
1159
- }
1160
- replaceLimit(limit) {
1161
- this.limitSubstitution = limit;
1162
- return this;
1163
- }
1164
- build() {
1165
- let commands = this.generateSetCommands();
1166
- commands += this.generateExplain().query;
1167
- if (this.isIntrospection) commands += " -- @qd_introspection";
1168
- return commands;
1169
- }
1170
- /** Return the "set a=b" parts of the command in the query separate from the explain select ... part */
1171
- buildParts() {
1172
- const commands = this.generateSetCommands();
1173
- const explain = this.generateExplain();
1174
- this._preamble = explain.preamble;
1175
- if (this.isIntrospection) explain.query += " -- @qd_introspection";
1176
- return {
1177
- commands,
1178
- query: explain.query
1179
- };
1180
- }
1181
- generateSetCommands() {
1182
- let commands = "";
1183
- for (const key in this.commands) {
1184
- const value = this.commands[key];
1185
- commands += `set local ${key}=${value};\n`;
1186
- }
1187
- return commands;
1188
- }
1189
- generateExplain() {
1190
- let finalQuery = "";
1191
- if (this.explainFlags.length > 0) finalQuery += `explain (${this.explainFlags.join(", ")}) `;
1192
- const query = this.substituteQuery();
1193
- const semicolon = query.endsWith(";") ? "" : ";";
1194
- const preamble = finalQuery.length;
1195
- finalQuery += `${query}${semicolon}`;
1196
- return {
1197
- query: finalQuery,
1198
- preamble
1199
- };
1200
- }
1201
- substituteQuery() {
1202
- let query = this.query;
1203
- if (this.limitSubstitution !== void 0) query = query.replace(/limit\s+\$\d+/g, `limit ${this.limitSubstitution}`);
1204
- for (const [key, value] of Object.entries(this.parameters)) query = query.replaceAll(`\\$${key}`, value.toString());
1205
- return query;
1206
- }
1207
- };
1208
- //#endregion
1209
- //#region src/sql/pg-identifier.ts
1210
- /**
1211
- * Represents an identifier in postgres that is subject
1212
- * to quoting rules. The {@link toString} rule behaves
1213
- * exactly like calling `select quote_ident($1)` in postgres
1214
- */
1215
- var PgIdentifier = class PgIdentifier {
1216
- constructor(value, quoted) {
1217
- this.value = value;
1218
- this.quoted = quoted;
1219
- }
1220
- /**
1221
- * Constructs an identifier from a single part (column or table name).
1222
- * When quoting identifiers like `select table.col` use {@link fromParts} instead
1223
- */
1224
- static fromString(identifier) {
1225
- const identifierRegex = /^[a-z_][a-z0-9_]*$/;
1226
- const match = identifier.match(/^"(.+)"$/);
1227
- if (match) {
1228
- const value = match[1];
1229
- return new PgIdentifier(value, !identifierRegex.test(value) || this.reservedKeywords.has(value.toLowerCase()));
1230
- }
1231
- return new PgIdentifier(identifier, !identifierRegex.test(identifier) || this.reservedKeywords.has(identifier.toLowerCase()));
1232
- }
1233
- /**
1234
- * Quotes parts of an identifier like `select schema.table.col`.
1235
- * A separate function is necessary because postgres will treat
1236
- * `select "HELLO.WORLD"` as a column name. It has to be like
1237
- * `select "HELLO"."WORLD"` instead.
1238
- */
1239
- static fromParts(...identifiers) {
1240
- return new PgIdentifier(identifiers.map((identifier) => {
1241
- if (typeof identifier === "string") return PgIdentifier.fromString(identifier);
1242
- else return identifier;
1243
- }).join("."), false);
1244
- }
1245
- toString() {
1246
- if (this.quoted) return `"${this.value.replace(/"/g, "\"\"")}"`;
1247
- return this.value;
1248
- }
1249
- toJSON() {
1250
- return this.toString();
1251
- }
1252
- };
1253
- _defineProperty(PgIdentifier, "reservedKeywords", new Set([
1254
- "all",
1255
- "analyse",
1256
- "analyze",
1257
- "and",
1258
- "any",
1259
- "array",
1260
- "as",
1261
- "asc",
1262
- "asymmetric",
1263
- "authorization",
1264
- "between",
1265
- "bigint",
1266
- "binary",
1267
- "bit",
1268
- "boolean",
1269
- "both",
1270
- "case",
1271
- "cast",
1272
- "char",
1273
- "character",
1274
- "check",
1275
- "coalesce",
1276
- "collate",
1277
- "collation",
1278
- "column",
1279
- "concurrently",
1280
- "constraint",
1281
- "create",
1282
- "cross",
1283
- "current_catalog",
1284
- "current_date",
1285
- "current_role",
1286
- "current_schema",
1287
- "current_time",
1288
- "current_timestamp",
1289
- "current_user",
1290
- "dec",
1291
- "decimal",
1292
- "default",
1293
- "deferrable",
1294
- "desc",
1295
- "distinct",
1296
- "do",
1297
- "else",
1298
- "end",
1299
- "except",
1300
- "exists",
1301
- "extract",
1302
- "false",
1303
- "fetch",
1304
- "float",
1305
- "for",
1306
- "foreign",
1307
- "freeze",
1308
- "from",
1309
- "full",
1310
- "grant",
1311
- "greatest",
1312
- "group",
1313
- "grouping",
1314
- "having",
1315
- "ilike",
1316
- "in",
1317
- "initially",
1318
- "inner",
1319
- "inout",
1320
- "int",
1321
- "integer",
1322
- "intersect",
1323
- "interval",
1324
- "into",
1325
- "is",
1326
- "isnull",
1327
- "join",
1328
- "json",
1329
- "json_array",
1330
- "json_arrayagg",
1331
- "json_exists",
1332
- "json_object",
1333
- "json_objectagg",
1334
- "json_query",
1335
- "json_scalar",
1336
- "json_serialize",
1337
- "json_table",
1338
- "json_value",
1339
- "lateral",
1340
- "leading",
1341
- "least",
1342
- "left",
1343
- "like",
1344
- "limit",
1345
- "localtime",
1346
- "localtimestamp",
1347
- "merge_action",
1348
- "national",
1349
- "natural",
1350
- "nchar",
1351
- "none",
1352
- "normalize",
1353
- "not",
1354
- "notnull",
1355
- "null",
1356
- "nullif",
1357
- "numeric",
1358
- "offset",
1359
- "on",
1360
- "only",
1361
- "or",
1362
- "order",
1363
- "out",
1364
- "outer",
1365
- "overlaps",
1366
- "overlay",
1367
- "placing",
1368
- "position",
1369
- "precision",
1370
- "primary",
1371
- "real",
1372
- "references",
1373
- "returning",
1374
- "right",
1375
- "row",
1376
- "select",
1377
- "session_user",
1378
- "setof",
1379
- "similar",
1380
- "smallint",
1381
- "some",
1382
- "substring",
1383
- "symmetric",
1384
- "system_user",
1385
- "table",
1386
- "tablesample",
1387
- "then",
1388
- "time",
1389
- "timestamp",
1390
- "to",
1391
- "trailing",
1392
- "treat",
1393
- "trim",
1394
- "true",
1395
- "union",
1396
- "unique",
1397
- "user",
1398
- "using",
1399
- "values",
1400
- "varchar",
1401
- "variadic",
1402
- "verbose",
1403
- "when",
1404
- "where",
1405
- "window",
1406
- "with",
1407
- "xmlattributes",
1408
- "xmlconcat",
1409
- "xmlelement",
1410
- "xmlexists",
1411
- "xmlforest",
1412
- "xmlnamespaces",
1413
- "xmlparse",
1414
- "xmlpi",
1415
- "xmlroot",
1416
- "xmlserialize",
1417
- "xmltable"
1418
- ]));
1419
- //#endregion
1420
- //#region src/sql/permutations.ts
1421
- /**
1422
- * Create permutations of the array while sorting it from
1423
- * largest permutation to smallest.
1424
- *
1425
- * This is important when generating index permutations as
1426
- * postgres happens to prefer indexes with the latest
1427
- * creation date when the cost of using 2 are the same
1428
- **/
1429
- function permutationsWithDescendingLength(arr) {
1430
- const collected = [];
1431
- function collect(path, rest) {
1432
- for (let i = 0; i < rest.length; i++) {
1433
- const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];
1434
- const nextPath = [...path, rest[i]];
1435
- collected.push(nextPath);
1436
- collect(nextPath, nextRest);
1437
- }
1438
- }
1439
- collect([], arr);
1440
- collected.sort((a, b) => b.length - a.length);
1441
- return collected;
1442
- }
1443
- //#endregion
1444
- //#region src/optimizer/genalgo.ts
1445
- var IndexOptimizer = class IndexOptimizer {
1446
- constructor(db, statistics, existingIndexes, config = {}) {
1447
- this.db = db;
1448
- this.statistics = statistics;
1449
- this.existingIndexes = existingIndexes;
1450
- this.config = config;
1451
- }
1452
- async run(builder, indexes, beforeQuery) {
1453
- const baseExplain = await this.testQueryWithStats(builder, async (tx) => {
1454
- if (beforeQuery) await beforeQuery(tx);
1455
- });
1456
- const baseCost = Number(baseExplain.Plan["Total Cost"]);
1457
- if (baseCost === 0) return {
1458
- kind: "zero_cost_plan",
1459
- explainPlan: baseExplain.Plan
1460
- };
1461
- const toCreate = this.indexesToCreate(indexes);
1462
- const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
1463
- if (beforeQuery) await beforeQuery(tx);
1464
- for (const permutation of toCreate) {
1465
- const createIndex = PostgresQueryBuilder.createIndex(permutation.definition, permutation.name).introspect().build();
1466
- await tx.exec(createIndex);
1467
- }
1468
- });
1469
- const finalCost = Number(finalExplain.Plan["Total Cost"]);
1470
- if (this.config.debug) console.dir(finalExplain, { depth: null });
1471
- const deltaPercentage = (baseCost - finalCost) / baseCost * 100;
1472
- if (finalCost < baseCost) console.log(` 🎉🎉🎉 ${(0, colorette.green)(`+${deltaPercentage.toFixed(2).padStart(5, "0")}%`)}`);
1473
- else if (finalCost > baseCost) console.log(`${(0, colorette.red)(`-${Math.abs(deltaPercentage).toFixed(2).padStart(5, "0")}%`)} ${(0, colorette.gray)("If there's a better index, we haven't tried it")}`);
1474
- const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
1475
- const finalIndexes = this.findUsedIndexes(finalExplain.Plan);
1476
- const triedIndexes = new Map(toCreate.map((index) => [index.name.toString(), index]));
1477
- this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);
1478
- return {
1479
- kind: "ok",
1480
- baseCost,
1481
- finalCost,
1482
- newIndexes: finalIndexes.newIndexes,
1483
- existingIndexes: baseIndexes.existingIndexes,
1484
- triedIndexes,
1485
- baseExplainPlan: baseExplain.Plan,
1486
- explainPlan: finalExplain.Plan
1487
- };
1488
- }
1489
- async runWithoutIndexes(builder) {
1490
- return await this.testQueryWithStats(builder, async (tx) => {
1491
- await this.dropExistingIndexes(tx);
1492
- });
1493
- }
1494
- /**
1495
- * Given the current indexes in the optimizer, transform them in some
1496
- * way to change which indexes will be assumed to exist when optimizing
1497
- *
1498
- * @example
1499
- * ```
1500
- * // resets indexes
1501
- * optimizer.transformIndexes(() => [])
1502
- *
1503
- * // adds new index
1504
- * optimizer.transformIndexes(indexes => [...indexes, newIndex])
1505
- * ```
1506
- */
1507
- transformIndexes(f) {
1508
- this.existingIndexes = f(this.existingIndexes);
1509
- return this;
1510
- }
1511
- /**
1512
- * Postgres has a limit of 63 characters for index names.
1513
- * So we use this to make sure we don't derive it from a list of columns that can
1514
- * overflow that limit.
1515
- */
1516
- indexName() {
1517
- const indexName = IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);
1518
- return PgIdentifier.fromString(indexName);
1519
- }
1520
- indexAlreadyExists(table, columns) {
1521
- 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) => {
1522
- if (columns[i].column !== c.name) return false;
1523
- if (columns[i].where) return false;
1524
- if (columns[i].sort) switch (columns[i].sort.dir) {
1525
- case "SORTBY_DEFAULT":
1526
- case "SORTBY_ASC":
1527
- if (c.order !== "ASC") return false;
1528
- break;
1529
- case "SORTBY_DESC":
1530
- if (c.order !== "DESC") return false;
1531
- break;
1532
- }
1533
- return true;
1534
- }));
1535
- }
1536
- /**
1537
- * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
1538
- **/
1539
- indexesToCreate(rootCandidates) {
1540
- const expressionCandidates = rootCandidates.filter((c) => c.jsonbExtraction);
1541
- const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator && !c.jsonbExtraction);
1542
- const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
1543
- const nextStage = [];
1544
- const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
1545
- for (const permutation of permutedIndexes.values()) {
1546
- const { table: rawTable, schema: rawSchema, columns } = permutation;
1547
- const permutations = permutationsWithDescendingLength(columns);
1548
- for (const columns of permutations) {
1549
- const schema = PgIdentifier.fromString(rawSchema);
1550
- const table = PgIdentifier.fromString(rawTable);
1551
- if (this.indexAlreadyExists(table.toString(), columns)) continue;
1552
- const indexName = this.indexName();
1553
- const definition = this.toDefinition({
1554
- table,
1555
- schema,
1556
- columns
1557
- }).raw;
1558
- nextStage.push({
1559
- name: indexName,
1560
- schema: schema.toString(),
1561
- table: table.toString(),
1562
- columns,
1563
- definition
1564
- });
1565
- }
1566
- }
1567
- const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
1568
- for (const group of ginGroups.values()) {
1569
- const { schema: rawSchema, table: rawTable, column, operators } = group;
1570
- const schema = PgIdentifier.fromString(rawSchema);
1571
- const table = PgIdentifier.fromString(rawTable);
1572
- const opclass = operators.some((op) => op !== "@>") ? void 0 : "jsonb_path_ops";
1573
- if (this.ginIndexAlreadyExists(table.toString(), column)) continue;
1574
- const indexName = this.indexName();
1575
- const candidate = {
1576
- schema: rawSchema,
1577
- table: rawTable,
1578
- column
1579
- };
1580
- const definition = this.toGinDefinition({
1581
- table,
1582
- schema,
1583
- column: PgIdentifier.fromString(column),
1584
- opclass
1585
- });
1586
- nextStage.push({
1587
- name: indexName,
1588
- schema: schema.toString(),
1589
- table: table.toString(),
1590
- columns: [candidate],
1591
- definition,
1592
- indexMethod: "gin",
1593
- opclass
1594
- });
1595
- }
1596
- const seenExpressions = /* @__PURE__ */ new Set();
1597
- for (const candidate of expressionCandidates) {
1598
- const expression = candidate.jsonbExtraction;
1599
- const key = `${candidate.schema}.${candidate.table}.${expression}`;
1600
- if (seenExpressions.has(key)) continue;
1601
- seenExpressions.add(key);
1602
- const schema = PgIdentifier.fromString(candidate.schema);
1603
- const table = PgIdentifier.fromString(candidate.table);
1604
- const indexName = this.indexName();
1605
- const definition = this.toExpressionDefinition({
1606
- table,
1607
- schema,
1608
- expression
1609
- });
1610
- nextStage.push({
1611
- name: indexName,
1612
- schema: schema.toString(),
1613
- table: table.toString(),
1614
- columns: [candidate],
1615
- definition
1616
- });
1617
- }
1618
- return nextStage;
1619
- }
1620
- toDefinition({ schema, table, columns }) {
1621
- const make = (col, order, _where, _keyword) => {
1622
- let fullyQualifiedTable;
1623
- if (schema.toString() === "public") fullyQualifiedTable = table;
1624
- else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
1625
- return `${fullyQualifiedTable}(${columns.map((c) => {
1626
- const column = PgIdentifier.fromString(c.column);
1627
- const direction = c.sort && this.sortDirection(c.sort);
1628
- const nulls = c.sort && this.nullsOrder(c.sort);
1629
- let sort = col(column.toString());
1630
- if (direction) sort += ` ${order(direction)}`;
1631
- if (nulls) sort += ` ${order(nulls)}`;
1632
- return sort;
1633
- }).join(", ")})`;
1634
- };
1635
- const id = (a) => a;
1636
- return {
1637
- raw: make(id, id, id, id),
1638
- colored: make(colorette.green, colorette.yellow, colorette.magenta, colorette.blue)
1639
- };
1640
- }
1641
- toGinDefinition({ schema, table, column, opclass }) {
1642
- let fullyQualifiedTable;
1643
- if (schema.toString() === "public") fullyQualifiedTable = table;
1644
- else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
1645
- const opclassSuffix = opclass ? ` ${opclass}` : "";
1646
- return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
1647
- }
1648
- toExpressionDefinition({ schema, table, expression }) {
1649
- let fullyQualifiedTable;
1650
- if (schema.toString() === "public") fullyQualifiedTable = table;
1651
- else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
1652
- return `${fullyQualifiedTable}((${expression}))`;
1653
- }
1654
- groupGinCandidatesByColumn(candidates) {
1655
- const groups = /* @__PURE__ */ new Map();
1656
- for (const c of candidates) {
1657
- if (!c.jsonbOperator) continue;
1658
- const key = `${c.schema}.${c.table}.${c.column}`;
1659
- const existing = groups.get(key);
1660
- if (existing) {
1661
- if (!existing.operators.includes(c.jsonbOperator)) existing.operators.push(c.jsonbOperator);
1662
- } else groups.set(key, {
1663
- schema: c.schema,
1664
- table: c.table,
1665
- column: c.column,
1666
- operators: [c.jsonbOperator]
1667
- });
1668
- }
1669
- return groups;
1670
- }
1671
- ginIndexAlreadyExists(table, column) {
1672
- return this.existingIndexes.find((index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column));
1673
- }
1674
- /**
1675
- * Drop indexes that can be dropped. Ignore the ones that can't
1676
- */
1677
- async dropExistingIndexes(tx) {
1678
- for (const index of this.existingIndexes) {
1679
- if (!isIndexProbablyDroppable(index)) continue;
1680
- await dropIndex(tx, PgIdentifier.fromParts(index.schema_name, index.index_name));
1681
- }
1682
- }
1683
- whereClause(c, col, keyword) {
1684
- if (!c.where) return "";
1685
- if (c.where.nulltest === "IS_NULL") return `${col(`"${c.column}"`)} is ${keyword("null")}`;
1686
- if (c.where.nulltest === "IS_NOT_NULL") return `${col(`"${c.column}"`)} is not ${keyword("null")}`;
1687
- return "";
1688
- }
1689
- nullsOrder(s) {
1690
- if (!s.nulls) return "";
1691
- switch (s.nulls) {
1692
- case "SORTBY_NULLS_FIRST": return "nulls first";
1693
- case "SORTBY_NULLS_LAST": return "nulls last";
1694
- default: return "";
1695
- }
1696
- }
1697
- sortDirection(s) {
1698
- if (!s.dir) return "";
1699
- switch (s.dir) {
1700
- case "SORTBY_DESC": return "desc";
1701
- case "SORTBY_ASC": return "asc";
1702
- default: return "";
1703
- }
1704
- }
1705
- async testQueryWithStats(builder, f, options) {
1706
- try {
1707
- await this.db.transaction(async (tx) => {
1708
- await f?.(tx);
1709
- await this.statistics.restoreStats(tx);
1710
- const flags = ["format json"];
1711
- if (options && !options.genericPlan) {
1712
- flags.push("analyze");
1713
- if (this.config.trace) flags.push("trace");
1714
- } else flags.push("generic_plan");
1715
- const { commands, query } = builder.introspect().explain(flags).buildParts();
1716
- await tx.exec(commands);
1717
- const explain = (await tx.exec(query, options?.params))[0]["QUERY PLAN"][0];
1718
- throw new RollbackError(explain);
1719
- });
1720
- } catch (error) {
1721
- if (error instanceof RollbackError) return error.value;
1722
- throw error;
1723
- }
1724
- throw new Error("Unreachable");
1725
- }
1726
- groupPotentialIndexColumnsByTable(indexes) {
1727
- const tableColumns = /* @__PURE__ */ new Map();
1728
- for (const index of indexes) {
1729
- const existing = tableColumns.get(`${index.schema}.${index.table}`);
1730
- if (existing) existing.columns.push(index);
1731
- else tableColumns.set(`${index.schema}.${index.table}`, {
1732
- table: index.table,
1733
- schema: index.schema,
1734
- columns: [index]
1735
- });
1736
- }
1737
- return tableColumns;
1738
- }
1739
- findUsedIndexes(explain) {
1740
- const newIndexes = /* @__PURE__ */ new Set();
1741
- const existingIndexes = /* @__PURE__ */ new Set();
1742
- const prefix = IndexOptimizer.prefix;
1743
- walkExplain(explain, (stage) => {
1744
- const indexName = stage["Index Name"];
1745
- if (indexName) if (indexName.startsWith(prefix)) newIndexes.add(indexName);
1746
- else if (indexName.includes(prefix)) {
1747
- const actualName = indexName.substring(indexName.indexOf(prefix));
1748
- newIndexes.add(actualName);
1749
- } else existingIndexes.add(indexName);
1750
- });
1751
- return {
1752
- newIndexes,
1753
- existingIndexes
1754
- };
1755
- }
1756
- replaceUsedIndexesWithDefinition(explain, triedIndexes) {
1757
- walkExplain(explain, (stage) => {
1758
- const indexName = stage["Index Name"];
1759
- if (typeof indexName === "string") {
1760
- const recommendation = triedIndexes.get(indexName);
1761
- if (recommendation) stage["Index Name"] = recommendation.definition;
1762
- }
1763
- });
1764
- }
1765
- };
1766
- _defineProperty(IndexOptimizer, "prefix", "__qd_");
1767
- function walkExplain(explain, f) {
1768
- function go(plan) {
1769
- f(plan);
1770
- if (plan.Plans) for (const p of plan.Plans) go(p);
1771
- }
1772
- go(explain);
1773
- }
1774
- var RollbackError = class {
1775
- constructor(value) {
1776
- this.value = value;
1777
- }
1778
- };
1779
- const PROCEED = Symbol("PROCEED");
1780
- const SKIP = Symbol("SKIP");
1781
- //#endregion
1782
- //#region src/optimizer/statistics.ts
1783
- const StatisticsSource = zod.z.union([zod.z.object({
1784
- kind: zod.z.literal("path"),
1785
- path: zod.z.string().min(1)
1786
- }), zod.z.object({ kind: zod.z.literal("inline") })]);
1787
- const ExportedStatsStatistics = zod.z.object({
1788
- stawidth: zod.z.number(),
1789
- stainherit: zod.z.boolean().default(false),
1790
- stadistinct: zod.z.number(),
1791
- stanullfrac: zod.z.number(),
1792
- stakind1: zod.z.number().min(0),
1793
- stakind2: zod.z.number().min(0),
1794
- stakind3: zod.z.number().min(0),
1795
- stakind4: zod.z.number().min(0),
1796
- stakind5: zod.z.number().min(0),
1797
- staop1: zod.z.string(),
1798
- staop2: zod.z.string(),
1799
- staop3: zod.z.string(),
1800
- staop4: zod.z.string(),
1801
- staop5: zod.z.string(),
1802
- stacoll1: zod.z.string(),
1803
- stacoll2: zod.z.string(),
1804
- stacoll3: zod.z.string(),
1805
- stacoll4: zod.z.string(),
1806
- stacoll5: zod.z.string(),
1807
- stanumbers1: zod.z.array(zod.z.number()).nullable(),
1808
- stanumbers2: zod.z.array(zod.z.number()).nullable(),
1809
- stanumbers3: zod.z.array(zod.z.number()).nullable(),
1810
- stanumbers4: zod.z.array(zod.z.number()).nullable(),
1811
- stanumbers5: zod.z.array(zod.z.number()).nullable(),
1812
- stavalues1: zod.z.array(zod.z.any()).nullable(),
1813
- stavalues2: zod.z.array(zod.z.any()).nullable(),
1814
- stavalues3: zod.z.array(zod.z.any()).nullable(),
1815
- stavalues4: zod.z.array(zod.z.any()).nullable(),
1816
- stavalues5: zod.z.array(zod.z.any()).nullable()
1817
- });
1818
- const ExportedStatsColumns = zod.z.object({
1819
- columnName: zod.z.string(),
1820
- attlen: zod.z.number().nullable(),
1821
- stats: ExportedStatsStatistics.nullable()
1822
- });
1823
- const ExportedStatsIndex = zod.z.object({
1824
- indexName: zod.z.string(),
1825
- amname: zod.z.string().default("btree"),
1826
- relpages: zod.z.number(),
1827
- reltuples: zod.z.number(),
1828
- relallvisible: zod.z.number(),
1829
- relallfrozen: zod.z.number().optional(),
1830
- fillfactor: zod.z.number().default(90),
1831
- columns: zod.z.array(zod.z.object({ attlen: zod.z.number().nullable() })).default([])
1832
- });
1833
- const ExportedStatsV1 = zod.z.object({
1834
- tableName: zod.z.string(),
1835
- schemaName: zod.z.string(),
1836
- relpages: zod.z.number(),
1837
- reltuples: zod.z.number(),
1838
- relallvisible: zod.z.number(),
1839
- relallfrozen: zod.z.number().optional(),
1840
- columns: zod.z.array(ExportedStatsColumns).default([]),
1841
- indexes: zod.z.array(ExportedStatsIndex)
1842
- });
1843
- const ExportedStats = zod.z.union([ExportedStatsV1]);
1844
- const StatisticsMode = zod.z.discriminatedUnion("kind", [zod.z.object({
1845
- kind: zod.z.literal("fromAssumption"),
1846
- reltuples: zod.z.number().min(0)
1847
- }), zod.z.object({
1848
- kind: zod.z.literal("fromStatisticsExport"),
1849
- stats: zod.z.array(ExportedStats),
1850
- source: StatisticsSource
1851
- })]);
1852
- const DEFAULT_RELTUPLES = 1e4;
1853
- const DEFAULT_RELPAGES = 1;
1854
- const DEFAULT_PAGE_SIZE = 2 ** 13;
1855
- function estimateStawidth(col) {
1856
- return col.attlen ?? 32;
1857
- }
1858
- function estimateRelpages(reltuples, columns) {
1859
- const rowWidth = columns.reduce((sum, col) => sum + estimateStawidth(col), 0) + 27;
1860
- return Math.ceil(reltuples * rowWidth / DEFAULT_PAGE_SIZE);
1861
- }
1862
- function estimateIndexRelpages(reltuples, columns, fillfactor, amname, tableRelpages) {
1863
- if (amname === "gin") return Math.ceil(tableRelpages * .3);
1864
- const keyWidth = columns.reduce((sum, col) => sum + estimateStawidth(col) + 16, 0);
1865
- return Math.ceil(reltuples * keyWidth / DEFAULT_PAGE_SIZE / fillfactor);
1866
- }
1867
- var Statistics = class Statistics {
1868
- constructor(db, postgresVersion, ownMetadata, statsMode) {
1869
- this.db = db;
1870
- this.postgresVersion = postgresVersion;
1871
- this.ownMetadata = ownMetadata;
1872
- _defineProperty(this, "mode", void 0);
1873
- _defineProperty(this, "exportedMetadata", void 0);
1874
- if (statsMode) {
1875
- this.mode = statsMode;
1876
- if (statsMode.kind === "fromStatisticsExport") this.exportedMetadata = statsMode.stats;
1877
- } else this.mode = Statistics.defaultStatsMode;
1878
- }
1879
- static statsModeFromAssumption({ reltuples }) {
1880
- return {
1881
- kind: "fromAssumption",
1882
- reltuples
1883
- };
1884
- }
1885
- /**
1886
- * Create a statistic mode from stats exported from another database
1887
- **/
1888
- static statsModeFromExport(stats) {
1889
- return {
1890
- kind: "fromStatisticsExport",
1891
- source: { kind: "inline" },
1892
- stats
1893
- };
1894
- }
1895
- static async fromPostgres(db, statsMode) {
1896
- const version = await db.serverNum();
1897
- return new Statistics(db, version, await Statistics.dumpStats(db, version, "full"), statsMode);
1898
- }
1899
- restoreStats(tx) {
1900
- return this.restoreStats17(tx);
1901
- }
1902
- approximateTotalRows() {
1903
- if (!this.exportedMetadata) return 0;
1904
- let totalRows = 0;
1905
- for (const table of this.exportedMetadata) totalRows += table.reltuples;
1906
- return totalRows;
1907
- }
1908
- /**
1909
- * We have to cast stavaluesN to the correct type
1910
- * This derives that type for us so it can be used in `array_in`
1911
- */
1912
- stavalueKind(values) {
1913
- if (!values || values.length === 0) return null;
1914
- const [elem] = values;
1915
- if (typeof elem === "number") return "real";
1916
- else if (typeof elem === "boolean") return "boolean";
1917
- return "text";
1918
- }
1919
- /**
1920
- * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
1921
- * for columns with array types (e.g. text[], int4[]). These create
1922
- * multidimensional arrays that can be "ragged" (sub-arrays with different
1923
- * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
1924
- * arrays from JSON, so we need to drop these values.
1925
- */
1926
- static safeStavalues(values) {
1927
- if (!values || values.length === 0) return values;
1928
- if (values.some((v) => Array.isArray(v))) {
1929
- console.warn("Discarding ragged multidimensional stavalues array");
1930
- return null;
1931
- }
1932
- return values;
1933
- }
1934
- async restoreStats17(tx) {
1935
- const warnings = {
1936
- tablesNotInExports: [],
1937
- tablesNotInTest: [],
1938
- tableNotAnalyzed: [],
1939
- statsMissing: []
1940
- };
1941
- const processedTables = /* @__PURE__ */ new Set();
1942
- let columnStatsUpdatePromise;
1943
- const columnStatsValues = [];
1944
- for (const table of this.ownMetadata) {
1945
- const target = (this.exportedMetadata?.find((m) => m.tableName === table.tableName && m.schemaName === table.schemaName))?.columns ?? table.columns;
1946
- for (const column of target) {
1947
- const { stats } = column;
1948
- if (!stats || this.mode.kind === "fromAssumption") {
1949
- const stawidth = stats?.stawidth || estimateStawidth(column);
1950
- columnStatsValues.push({
1951
- schema_name: table.schemaName,
1952
- table_name: table.tableName,
1953
- column_name: column.columnName,
1954
- stainherit: false,
1955
- stanullfrac: .04,
1956
- stawidth,
1957
- stadistinct: -.9,
1958
- stakind1: 0,
1959
- stakind2: 0,
1960
- stakind3: 3,
1961
- stakind4: 0,
1962
- stakind5: 0,
1963
- stacoll1: "0",
1964
- stacoll2: "0",
1965
- stacoll3: "0",
1966
- stacoll4: "0",
1967
- stacoll5: "0",
1968
- staop1: "0",
1969
- staop2: "0",
1970
- staop3: "0",
1971
- staop4: "0",
1972
- staop5: "0",
1973
- stanumbers1: null,
1974
- stanumbers2: null,
1975
- stanumbers3: [.9],
1976
- stanumbers4: null,
1977
- stanumbers5: null,
1978
- stavalues1: null,
1979
- stavalues2: null,
1980
- stavalues3: null,
1981
- stavalues4: null,
1982
- stavalues5: null,
1983
- _value_type1: "real",
1984
- _value_type2: "real",
1985
- _value_type3: "real",
1986
- _value_type4: "real",
1987
- _value_type5: "real"
1988
- });
1989
- continue;
1990
- }
1991
- columnStatsValues.push({
1992
- schema_name: table.schemaName,
1993
- table_name: table.tableName,
1994
- column_name: column.columnName,
1995
- stainherit: stats.stainherit ?? false,
1996
- stanullfrac: stats.stanullfrac,
1997
- stawidth: stats.stawidth,
1998
- stadistinct: stats.stadistinct,
1999
- stakind1: stats.stakind1,
2000
- stakind2: stats.stakind2,
2001
- stakind3: stats.stakind3,
2002
- stakind4: stats.stakind4,
2003
- stakind5: stats.stakind5,
2004
- staop1: stats.staop1,
2005
- staop2: stats.staop2,
2006
- staop3: stats.staop3,
2007
- staop4: stats.staop4,
2008
- staop5: stats.staop5,
2009
- stacoll1: stats.stacoll1,
2010
- stacoll2: stats.stacoll2,
2011
- stacoll3: stats.stacoll3,
2012
- stacoll4: stats.stacoll4,
2013
- stacoll5: stats.stacoll5,
2014
- stanumbers1: stats.stanumbers1,
2015
- stanumbers2: stats.stanumbers2,
2016
- stanumbers3: stats.stanumbers3,
2017
- stanumbers4: stats.stanumbers4,
2018
- stanumbers5: stats.stanumbers5,
2019
- stavalues1: Statistics.safeStavalues(stats.stavalues1),
2020
- stavalues2: Statistics.safeStavalues(stats.stavalues2),
2021
- stavalues3: Statistics.safeStavalues(stats.stavalues3),
2022
- stavalues4: Statistics.safeStavalues(stats.stavalues4),
2023
- stavalues5: Statistics.safeStavalues(stats.stavalues5),
2024
- _value_type1: this.stavalueKind(Statistics.safeStavalues(stats.stavalues1)),
2025
- _value_type2: this.stavalueKind(Statistics.safeStavalues(stats.stavalues2)),
2026
- _value_type3: this.stavalueKind(Statistics.safeStavalues(stats.stavalues3)),
2027
- _value_type4: this.stavalueKind(Statistics.safeStavalues(stats.stavalues4)),
2028
- _value_type5: this.stavalueKind(Statistics.safeStavalues(stats.stavalues5))
2029
- });
2030
- }
2031
- }
2032
- /**
2033
- * Postgres has 5 different slots for storing statistics per column and a potentially unlimited
2034
- * number of statistic types to choose from. Each code in `stakindN` can mean different things.
2035
- * Some statistics are just numerical values such as `n_distinct` and `correlation`, meaning
2036
- * they're only derived from `stanumbersN` and the value of `stanumbersN` is never read.
2037
- * Others take advantage of the `stavaluesN` columns which use `anyarray` type to store
2038
- * concrete values internally for things like histogram bounds.
2039
- * Unfortunately we cannot change anyarrays without a C extension.
2040
- *
2041
- * (1) = most common values
2042
- * (2) = scalar histogram
2043
- * (3) = correlation <- can change
2044
- * (4) = most common elements
2045
- * (5) = distinct elem count histogram <- can change
2046
- * (6) = length histogram (?) These don't appear in pg_stats
2047
- * (7) = bounds histogram (?) These don't appear in pg_stats
2048
- * (N) = potentially many more kinds of statistics. But postgres <=18 only uses these 7.
2049
- *
2050
- * What we're doing here is setting ANY statistic we cannot directly control
2051
- * (anything that relies on stavaluesN) to 0 to make sure the planner isn't influenced by what
2052
- * what the db collected from the test data.
2053
- * Because we do our tests with `generic_plan` it seems it's already unlikely that the planner will be
2054
- * using things like common values or histogram bounds to make the planning decisions we care about.
2055
- * This is a just in case.
2056
- */
2057
- const sql = dedent.default`
2058
- WITH input AS (
2059
- SELECT
2060
- c.oid AS starelid,
2061
- a.attnum AS staattnum,
2062
- v.stainherit,
2063
- v.stanullfrac,
2064
- v.stawidth,
2065
- v.stadistinct,
2066
- v.stakind1,
2067
- v.stakind2,
2068
- v.stakind3,
2069
- v.stakind4,
2070
- v.stakind5,
2071
- v.staop1,
2072
- v.staop2,
2073
- v.staop3,
2074
- v.staop4,
2075
- v.staop5,
2076
- v.stacoll1,
2077
- v.stacoll2,
2078
- v.stacoll3,
2079
- v.stacoll4,
2080
- v.stacoll5,
2081
- v.stanumbers1,
2082
- v.stanumbers2,
2083
- v.stanumbers3,
2084
- v.stanumbers4,
2085
- v.stanumbers5,
2086
- v.stavalues1,
2087
- v.stavalues2,
2088
- v.stavalues3,
2089
- v.stavalues4,
2090
- v.stavalues5,
2091
- _value_type1,
2092
- _value_type2,
2093
- _value_type3,
2094
- _value_type4,
2095
- _value_type5
2096
- FROM jsonb_to_recordset($1::jsonb) AS v(
2097
- schema_name text,
2098
- table_name text,
2099
- column_name text,
2100
- stainherit boolean,
2101
- stanullfrac real,
2102
- stawidth integer,
2103
- stadistinct real,
2104
- stakind1 real,
2105
- stakind2 real,
2106
- stakind3 real,
2107
- stakind4 real,
2108
- stakind5 real,
2109
- staop1 oid,
2110
- staop2 oid,
2111
- staop3 oid,
2112
- staop4 oid,
2113
- staop5 oid,
2114
- stacoll1 oid,
2115
- stacoll2 oid,
2116
- stacoll3 oid,
2117
- stacoll4 oid,
2118
- stacoll5 oid,
2119
- stanumbers1 real[],
2120
- stanumbers2 real[],
2121
- stanumbers3 real[],
2122
- stanumbers4 real[],
2123
- stanumbers5 real[],
2124
- stavalues1 text[],
2125
- stavalues2 text[],
2126
- stavalues3 text[],
2127
- stavalues4 text[],
2128
- stavalues5 text[],
2129
- _value_type1 text,
2130
- _value_type2 text,
2131
- _value_type3 text,
2132
- _value_type4 text,
2133
- _value_type5 text
2134
- )
2135
- JOIN pg_class c ON c.relname = v.table_name
2136
- JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name
2137
- JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name
2138
- ),
2139
- updated AS (
2140
- UPDATE pg_statistic s
2141
- SET
2142
- stanullfrac = i.stanullfrac,
2143
- stawidth = i.stawidth,
2144
- stadistinct = i.stadistinct,
2145
- stakind1 = i.stakind1,
2146
- stakind2 = i.stakind2,
2147
- stakind3 = i.stakind3,
2148
- stakind4 = i.stakind4,
2149
- stakind5 = i.stakind5,
2150
- staop1 = i.staop1,
2151
- staop2 = i.staop2,
2152
- staop3 = i.staop3,
2153
- staop4 = i.staop4,
2154
- staop5 = i.staop5,
2155
- stacoll1 = i.stacoll1,
2156
- stacoll2 = i.stacoll2,
2157
- stacoll3 = i.stacoll3,
2158
- stacoll4 = i.stacoll4,
2159
- stacoll5 = i.stacoll5,
2160
- stanumbers1 = i.stanumbers1,
2161
- stanumbers2 = i.stanumbers2,
2162
- stanumbers3 = i.stanumbers3,
2163
- stanumbers4 = i.stanumbers4,
2164
- stanumbers5 = i.stanumbers5,
2165
- stavalues1 = case
2166
- when i.stavalues1 is null then null
2167
- else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)
2168
- end,
2169
- stavalues2 = case
2170
- when i.stavalues2 is null then null
2171
- else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)
2172
- end,
2173
- stavalues3 = case
2174
- when i.stavalues3 is null then null
2175
- else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)
2176
- end,
2177
- stavalues4 = case
2178
- when i.stavalues4 is null then null
2179
- else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)
2180
- end,
2181
- stavalues5 = case
2182
- when i.stavalues5 is null then null
2183
- else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)
2184
- end
2185
- -- stavalues1 = i.stavalues1,
2186
- -- stavalues2 = i.stavalues2,
2187
- -- stavalues3 = i.stavalues3,
2188
- -- stavalues4 = i.stavalues4,
2189
- -- stavalues5 = i.stavalues5
2190
- FROM input i
2191
- WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit
2192
- RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5
2193
- ),
2194
- inserted as (
2195
- INSERT INTO pg_statistic (
2196
- starelid, staattnum, stainherit,
2197
- stanullfrac, stawidth, stadistinct,
2198
- stakind1, stakind2, stakind3, stakind4, stakind5,
2199
- staop1, staop2, staop3, staop4, staop5,
2200
- stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
2201
- stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
2202
- stavalues1, stavalues2, stavalues3, stavalues4, stavalues5
2203
- )
2204
- SELECT
2205
- i.starelid, i.staattnum, i.stainherit,
2206
- i.stanullfrac, i.stawidth, i.stadistinct,
2207
- i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,
2208
- i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,
2209
- i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,
2210
- i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,
2211
- -- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,
2212
- case
2213
- when i.stavalues1 is null then null
2214
- else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)
2215
- end,
2216
- case
2217
- when i.stavalues2 is null then null
2218
- else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)
2219
- end,
2220
- case
2221
- when i.stavalues3 is null then null
2222
- else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)
2223
- end,
2224
- case
2225
- when i.stavalues4 is null then null
2226
- else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)
2227
- end,
2228
- case
2229
- when i.stavalues5 is null then null
2230
- else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)
2231
- end
2232
- -- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5
2233
- FROM input i
2234
- LEFT JOIN updated u
2235
- ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit
2236
- WHERE u.starelid IS NULL
2237
- returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5
2238
- )
2239
- select * from updated union all (select * from inserted); -- @qd_introspection`;
2240
- columnStatsUpdatePromise = tx.exec(sql, [columnStatsValues]).catch((err) => {
2241
- console.error("Something wrong wrong updating column stats");
2242
- console.error(err);
2243
- throw err;
2244
- });
2245
- const reltuplesValues = [];
2246
- for (const table of this.ownMetadata) {
2247
- processedTables.add(`${table.schemaName}.${table.tableName}`);
2248
- let targetTable;
2249
- if (this.exportedMetadata) targetTable = this.exportedMetadata.find((m) => m.tableName === table.tableName && m.schemaName === table.schemaName);
2250
- else targetTable = table;
2251
- let reltuples;
2252
- let relpages;
2253
- let relallvisible = 0;
2254
- let relallfrozen;
2255
- if (this.mode.kind === "fromAssumption") {
2256
- reltuples = this.mode.reltuples;
2257
- relpages = estimateRelpages(reltuples, table.columns);
2258
- } else if (targetTable) {
2259
- reltuples = targetTable.reltuples;
2260
- relpages = targetTable.relpages;
2261
- relallvisible = targetTable.relallvisible;
2262
- relallfrozen = targetTable.relallfrozen;
2263
- } else {
2264
- warnings.tablesNotInExports.push(`${table.schemaName}.${table.tableName}`);
2265
- reltuples = DEFAULT_RELTUPLES;
2266
- relpages = DEFAULT_RELPAGES;
2267
- }
2268
- reltuplesValues.push({
2269
- relname: table.tableName,
2270
- schema_name: table.schemaName,
2271
- reltuples,
2272
- relpages,
2273
- relallfrozen,
2274
- relallvisible
2275
- });
2276
- if (this.mode.kind === "fromAssumption") for (const index of table.indexes) {
2277
- const indexRelpages = estimateIndexRelpages(this.mode.reltuples, index.columns, index.fillfactor / 100, index.amname, relpages);
2278
- reltuplesValues.push({
2279
- relname: index.indexName,
2280
- schema_name: table.schemaName,
2281
- reltuples: this.mode.reltuples,
2282
- relpages: indexRelpages,
2283
- relallfrozen: 0,
2284
- relallvisible: indexRelpages
2285
- });
2286
- }
2287
- else if (targetTable) for (const index of targetTable.indexes) reltuplesValues.push({
2288
- relname: index.indexName,
2289
- schema_name: targetTable.schemaName,
2290
- reltuples: index.reltuples,
2291
- relpages: index.relpages,
2292
- relallfrozen: index.relallfrozen,
2293
- relallvisible: index.relallvisible
2294
- });
2295
- }
2296
- const reltuplesQuery = dedent.default`
2297
- update pg_class p
2298
- set reltuples = v.reltuples,
2299
- relpages = v.relpages,
2300
- -- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,
2301
- relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end
2302
- from jsonb_to_recordset($1::jsonb)
2303
- as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)
2304
- where p.relname = v.relname
2305
- and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)
2306
- returning p.relname, p.relnamespace, p.reltuples, p.relpages;
2307
- `;
2308
- const reltuplesPromise = tx.exec(reltuplesQuery, [reltuplesValues]).catch((err) => {
2309
- console.error("Something went wrong updating reltuples/relpages");
2310
- console.error(err);
2311
- return err;
2312
- });
2313
- if (this.exportedMetadata) for (const table of this.exportedMetadata) {
2314
- const tableExists = processedTables.has(`${table.schemaName}.${table.tableName}`);
2315
- if (tableExists && table.reltuples === -1) {
2316
- console.warn(`Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`);
2317
- warnings.tableNotAnalyzed.push(`${table.schemaName}.${table.tableName}`);
2318
- }
2319
- if (tableExists) continue;
2320
- warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);
2321
- }
2322
- const [statsUpdates, reltuplesUpdates] = await Promise.all([columnStatsUpdatePromise, reltuplesPromise]);
2323
- if (!(statsUpdates ? statsUpdates.length === columnStatsValues.length : true)) console.error(`Did not update expected column stats`);
2324
- if (reltuplesUpdates.length !== reltuplesValues.length) console.error(`Did not update expected reltuples/relpages`);
2325
- return warnings;
2326
- }
2327
- static async dumpStats(db, postgresVersion, kind) {
2328
- const fullDump = kind === "full";
2329
- console.log(`dumping stats for postgres ${(0, colorette.gray)(postgresVersion)}`);
2330
- const stats = await db.exec(`
2331
- WITH table_columns AS (
2332
- SELECT
2333
- cl.relname,
2334
- n.nspname,
2335
- cl.reltuples,
2336
- cl.relpages,
2337
- cl.relallvisible,
2338
- -- cl.relallfrozen,
2339
- json_agg(
2340
- json_build_object(
2341
- 'columnName', a.attname,
2342
- 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END,
2343
- 'stats', (
2344
- SELECT json_build_object(
2345
- 'starelid', s.starelid,
2346
- 'staattnum', s.staattnum,
2347
- 'stanullfrac', s.stanullfrac,
2348
- 'stawidth', s.stawidth,
2349
- 'stadistinct', s.stadistinct,
2350
- 'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,
2351
- 'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,
2352
- 'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,
2353
- 'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,
2354
- 'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,
2355
- 'stavalues1', CASE WHEN $1 THEN s.stavalues1 ELSE NULL END,
2356
- 'stavalues2', CASE WHEN $1 THEN s.stavalues2 ELSE NULL END,
2357
- 'stavalues3', CASE WHEN $1 THEN s.stavalues3 ELSE NULL END,
2358
- 'stavalues4', CASE WHEN $1 THEN s.stavalues4 ELSE NULL END,
2359
- 'stavalues5', CASE WHEN $1 THEN s.stavalues5 ELSE NULL END
2360
- )
2361
- FROM pg_statistic s
2362
- WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum
2363
- )
2364
- )
2365
- ORDER BY a.attnum
2366
- ) AS columns
2367
- FROM pg_class cl
2368
- JOIN pg_namespace n ON n.oid = cl.relnamespace
2369
- JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attnum > 0 AND NOT a.attisdropped
2370
- WHERE cl.relkind = 'r'
2371
- AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'tiger', 'tiger_data', 'topology')
2372
- AND cl.relname NOT IN ('pg_stat_statements', 'pg_stat_statements_info')
2373
- GROUP BY cl.relname, n.nspname, cl.reltuples, cl.relpages, cl.relallvisible
2374
- ),
2375
- table_indexes AS (
2376
- SELECT
2377
- t.relname AS table_name,
2378
- json_agg(
2379
- json_build_object(
2380
- 'indexName', i.relname,
2381
- 'amname', am.amname,
2382
- 'reltuples', i.reltuples,
2383
- 'relpages', i.relpages,
2384
- 'relallvisible', i.relallvisible,
2385
- -- 'relallfrozen', i.relallfrozen,
2386
- 'fillfactor', COALESCE(
2387
- (
2388
- SELECT (regexp_match(opt, 'fillfactor=(\\d+)'))[1]::integer
2389
- FROM unnest(i.reloptions) AS opt
2390
- WHERE opt LIKE 'fillfactor=%'
2391
- LIMIT 1
2392
- ),
2393
- 90
2394
- ),
2395
- 'columns', COALESCE(
2396
- (
2397
- SELECT json_agg(json_build_object(
2398
- 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END
2399
- ) ORDER BY col_pos.ord)
2400
- FROM unnest(ix.indkey) WITH ORDINALITY AS col_pos(attnum, ord)
2401
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = col_pos.attnum
2402
- WHERE col_pos.attnum > 0
2403
- ),
2404
- '[]'::json
2405
- )
2406
- )
2407
- ) AS indexes
2408
- FROM pg_class t
2409
- JOIN pg_index ix ON ix.indrelid = t.oid
2410
- JOIN pg_class i ON i.oid = ix.indexrelid
2411
- JOIN pg_am am ON am.oid = i.relam
2412
- JOIN pg_namespace n ON n.oid = t.relnamespace
2413
- WHERE t.relname NOT LIKE 'pg_%'
2414
- AND n.nspname <> 'information_schema'
2415
- AND n.nspname NOT IN ('tiger', 'tiger_data', 'topology')
2416
- GROUP BY t.relname
2417
- )
2418
- SELECT json_agg(
2419
- json_build_object(
2420
- 'tableName', tc.relname,
2421
- 'schemaName', tc.nspname,
2422
- 'reltuples', tc.reltuples,
2423
- 'relpages', tc.relpages,
2424
- 'relallvisible', tc.relallvisible,
2425
- -- 'relallfrozen', tc.relallfrozen,
2426
- 'columns', COALESCE(tc.columns, '[]'::json),
2427
- 'indexes', COALESCE(ti.indexes, '[]'::json)
2428
- )
2429
- )
2430
- FROM table_columns tc
2431
- LEFT JOIN table_indexes ti
2432
- ON ti.table_name = tc.relname;
2433
- `, [fullDump]);
2434
- return zod.z.array(ExportedStats).parse(stats[0].json_agg);
2435
- }
2436
- /**
2437
- * Returns all indexes in the database.
2438
- * ONLY handles regular btree indexes
2439
- */
2440
- async getExistingIndexes() {
2441
- return await this.db.exec(`
2442
- WITH partitioned_tables AS (
2443
- SELECT
2444
- inhparent::regclass AS parent_table,
2445
- inhrelid::regclass AS partition_table
2446
- FROM
2447
- pg_inherits
2448
- )
2449
- SELECT
2450
- n.nspname AS schema_name,
2451
- COALESCE(pt.parent_table::text, t.relname) AS table_name,
2452
- i.relname AS index_name,
2453
- ix.indisprimary as is_primary,
2454
- ix.indisunique as is_unique,
2455
- am.amname AS index_type,
2456
- array_agg(
2457
- CASE
2458
- -- Handle regular columns
2459
- WHEN a.attname IS NOT NULL THEN
2460
- json_build_object('name', a.attname, 'order',
2461
- CASE
2462
- WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
2463
- ELSE 'ASC'
2464
- END,
2465
- 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
2466
- -- Handle expressions
2467
- ELSE
2468
- json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
2469
- CASE
2470
- WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
2471
- ELSE 'ASC'
2472
- END,
2473
- 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
2474
- END
2475
- ORDER BY array_position(ix.indkey, k.attnum)
2476
- ) AS index_columns
2477
- FROM
2478
- pg_class t
2479
- LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table
2480
- JOIN pg_index ix ON t.oid = ix.indrelid
2481
- JOIN pg_class i ON i.oid = ix.indexrelid
2482
- JOIN pg_am am ON i.relam = am.oid
2483
- LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
2484
- LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
2485
- LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
2486
- JOIN pg_namespace n ON t.relnamespace = n.oid
2487
- WHERE
2488
- n.nspname not like 'pg_%' and
2489
- n.nspname <> 'information_schema'
2490
- GROUP BY
2491
- n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique
2492
- ORDER BY
2493
- COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection
2494
- `);
2495
- }
2496
- };
2497
- _defineProperty(Statistics, "defaultStatsMode", Object.freeze({
2498
- kind: "fromAssumption",
2499
- reltuples: DEFAULT_RELTUPLES,
2500
- relpages: DEFAULT_RELPAGES
2501
- }));
2502
- //#endregion
2503
- //#region src/optimizer/pss-rewriter.ts
2504
- /**
2505
- * Rewriter for pg_stat_statements queries.
2506
- * Not all queries found in pg_stat_statements can be
2507
- * directly sent back to the database without first being rewritten.
2508
- */
2509
- var PssRewriter = class {
2510
- constructor() {
2511
- _defineProperty(this, "problematicKeywords", [
2512
- "interval",
2513
- "timestamp",
2514
- "geometry"
2515
- ]);
2516
- }
2517
- rewrite(query) {
2518
- return this.rewriteKeywordWithParameter(query);
2519
- }
2520
- rewriteKeywordWithParameter(query) {
2521
- return query.replace(/\b(\w+) (\$\d+)\b/gi, (match) => {
2522
- const [keyword, parameter] = match.split(" ");
2523
- if (!this.problematicKeywords.includes(keyword.toLowerCase())) return match;
2524
- return `(${parameter}::${keyword.toLowerCase()})`;
2525
- });
2526
- }
2527
- };
2528
- //#endregion
2529
- exports.Analyzer = Analyzer;
2530
- exports.ExportedStats = ExportedStats;
2531
- exports.ExportedStatsColumns = ExportedStatsColumns;
2532
- exports.ExportedStatsIndex = ExportedStatsIndex;
2533
- exports.ExportedStatsStatistics = ExportedStatsStatistics;
2534
- exports.ExportedStatsV1 = ExportedStatsV1;
2535
- exports.IndexOptimizer = IndexOptimizer;
2536
- exports.PROCEED = PROCEED;
2537
- exports.PgIdentifier = PgIdentifier;
2538
- exports.PostgresExplainStageSchema = PostgresExplainStageSchema;
2539
- exports.PostgresQueryBuilder = PostgresQueryBuilder;
2540
- exports.PostgresVersion = PostgresVersion;
2541
- exports.PssRewriter = PssRewriter;
2542
- exports.SKIP = SKIP;
2543
- exports.Statistics = Statistics;
2544
- exports.StatisticsMode = StatisticsMode;
2545
- exports.StatisticsSource = StatisticsSource;
2546
- exports.dropIndex = dropIndex;
2547
- exports.ignoredIdentifier = ignoredIdentifier;
2548
- exports.isIndexProbablyDroppable = isIndexProbablyDroppable;
2549
- exports.isIndexSupported = isIndexSupported;
2550
- exports.parseNudges = parseNudges;
2551
-
2552
- //# sourceMappingURL=index.cjs.map
3
+ const require_nudges = require("./sql/nudges.cjs");
4
+ const require_analyzer = require("./sql/analyzer.cjs");
5
+ const require_database = require("./sql/database.cjs");
6
+ const require_indexes = require("./sql/indexes.cjs");
7
+ const require_builder = require("./sql/builder.cjs");
8
+ const require_pg_identifier = require("./sql/pg-identifier.cjs");
9
+ const require_genalgo = require("./optimizer/genalgo.cjs");
10
+ const require_statistics = require("./optimizer/statistics.cjs");
11
+ const require_pss_rewriter = require("./optimizer/pss-rewriter.cjs");
12
+ const require_sentry = require("./sentry.cjs");
13
+ exports.Analyzer = require_analyzer.Analyzer;
14
+ exports.ExportedStats = require_statistics.ExportedStats;
15
+ exports.ExportedStatsColumns = require_statistics.ExportedStatsColumns;
16
+ exports.ExportedStatsIndex = require_statistics.ExportedStatsIndex;
17
+ exports.ExportedStatsStatistics = require_statistics.ExportedStatsStatistics;
18
+ exports.ExportedStatsV1 = require_statistics.ExportedStatsV1;
19
+ exports.IndexOptimizer = require_genalgo.IndexOptimizer;
20
+ exports.PROCEED = require_genalgo.PROCEED;
21
+ exports.PgIdentifier = require_pg_identifier.PgIdentifier;
22
+ exports.PostgresExplainStageSchema = require_database.PostgresExplainStageSchema;
23
+ exports.PostgresQueryBuilder = require_builder.PostgresQueryBuilder;
24
+ exports.PostgresVersion = require_database.PostgresVersion;
25
+ exports.PssRewriter = require_pss_rewriter.PssRewriter;
26
+ exports.SKIP = require_genalgo.SKIP;
27
+ exports.Statistics = require_statistics.Statistics;
28
+ exports.StatisticsMode = require_statistics.StatisticsMode;
29
+ exports.StatisticsSource = require_statistics.StatisticsSource;
30
+ exports.deriveSentryEnvironment = require_sentry.deriveSentryEnvironment;
31
+ exports.dropIndex = require_database.dropIndex;
32
+ exports.ignoredIdentifier = require_analyzer.ignoredIdentifier;
33
+ exports.isIndexProbablyDroppable = require_indexes.isIndexProbablyDroppable;
34
+ exports.isIndexSupported = require_indexes.isIndexSupported;
35
+ exports.parseNudges = require_nudges.parseNudges;