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