@query-doctor/core 0.4.1 → 0.4.2

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