@query-doctor/core 0.6.0-rc.4 → 0.7.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
- 'use client'
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  //#region \0rolldown/runtime.js
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -8,16 +8,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
8
8
  var __getProtoOf = Object.getPrototypeOf;
9
9
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
10
  var __copyProps = (to, from, except, desc) => {
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
- }
11
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
21
17
  }
22
18
  return to;
23
19
  };
@@ -25,30 +21,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
21
  value: mod,
26
22
  enumerable: true
27
23
  }) : target, mod));
28
-
29
24
  //#endregion
30
25
  let colorette = require("colorette");
31
26
  let pgsql_deparser = require("pgsql-deparser");
32
27
  let zod = require("zod");
33
28
  let dedent = require("dedent");
34
29
  dedent = __toESM(dedent);
35
-
36
- //#region src/sql/nudges.ts
37
- function is$1(node, kind) {
30
+ //#region src/sql/ast-utils.ts
31
+ function is(node, kind) {
38
32
  return kind in node;
39
33
  }
40
- function isANode$1(node) {
34
+ function isANode(node) {
41
35
  if (typeof node !== "object" || node === null) return false;
42
36
  const keys = Object.keys(node);
43
37
  return keys.length === 1 && /^[A-Z]/.test(keys[0]);
44
38
  }
39
+ function getNodeKind(node) {
40
+ return Object.keys(node)[0];
41
+ }
42
+ //#endregion
43
+ //#region src/sql/nudges.ts
45
44
  const findFuncCallsOnColumns = (whereClause) => {
46
45
  const nudges = [];
47
46
  Walker.shallowMatch(whereClause, "FuncCall", (node) => {
48
47
  if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) nudges.push({
49
48
  kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
50
49
  severity: "WARNING",
51
- message: "Avoid using functions on columns in WHERE clause",
50
+ message: "Avoid using functions on columns in conditions — prevents index usage",
52
51
  location: node.FuncCall.location
53
52
  });
54
53
  });
@@ -56,10 +55,18 @@ const findFuncCallsOnColumns = (whereClause) => {
56
55
  if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) nudges.push({
57
56
  kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
58
57
  severity: "WARNING",
59
- message: "Avoid using functions on columns in WHERE clause",
58
+ message: "Avoid using functions on columns in conditions — prevents index usage",
60
59
  location: node.CoalesceExpr.location
61
60
  });
62
61
  });
62
+ Walker.shallowMatch(whereClause, "TypeCast", (node) => {
63
+ if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) nudges.push({
64
+ kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
65
+ severity: "WARNING",
66
+ message: "Avoid using functions on columns in WHERE clause",
67
+ location: node.TypeCast.location
68
+ });
69
+ });
63
70
  return nudges;
64
71
  };
65
72
  /**
@@ -68,38 +75,38 @@ const findFuncCallsOnColumns = (whereClause) => {
68
75
  */
69
76
  function parseNudges(node, stack) {
70
77
  const nudges = [];
71
- if (is$1(node, "SelectStmt")) {
78
+ if (is(node, "SelectStmt")) {
72
79
  if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
73
80
  const star = node.SelectStmt.targetList?.find((target) => {
74
- if (!(is$1(target, "ResTarget") && target.ResTarget.val && is$1(target.ResTarget.val, "ColumnRef"))) return false;
81
+ if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) return false;
75
82
  const fields = target.ResTarget.val.ColumnRef.fields;
76
- if (!fields?.some((field) => is$1(field, "A_Star"))) return false;
83
+ if (!fields?.some((field) => is(field, "A_Star"))) return false;
77
84
  if (fields.length > 1) return false;
78
85
  return true;
79
86
  });
80
87
  if (star) {
81
88
  const fromClause = node.SelectStmt.fromClause;
82
- if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is$1(item, "RangeSubselect")))) nudges.push({
89
+ if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is(item, "RangeSubselect")))) nudges.push({
83
90
  kind: "AVOID_SELECT_STAR",
84
91
  severity: "INFO",
85
92
  message: "Avoid using SELECT *",
86
93
  location: star.ResTarget.location
87
94
  });
88
95
  }
89
- 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({
96
+ for (const target of node.SelectStmt.targetList ?? []) if (is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "SubLink") && target.ResTarget.val.SubLink.subLinkType === "EXPR_SUBLINK") nudges.push({
90
97
  kind: "AVOID_SCALAR_SUBQUERY_IN_SELECT",
91
98
  severity: "WARNING",
92
99
  message: "Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN",
93
100
  location: target.ResTarget.val.SubLink.location
94
101
  });
95
102
  }
96
- if (is$1(node, "SelectStmt")) {
103
+ if (is(node, "SelectStmt")) {
97
104
  if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
98
105
  if (node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0) {
99
106
  if (node.SelectStmt.fromClause.some((fromItem) => {
100
- return is$1(fromItem, "RangeVar") || is$1(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
107
+ return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
101
108
  })) {
102
- const fromLocation = node.SelectStmt.fromClause.find((item) => is$1(item, "RangeVar"))?.RangeVar.location;
109
+ const fromLocation = node.SelectStmt.fromClause.find((item) => is(item, "RangeVar"))?.RangeVar.location;
103
110
  if (!node.SelectStmt.whereClause) nudges.push({
104
111
  kind: "MISSING_WHERE_CLAUSE",
105
112
  severity: "INFO",
@@ -116,12 +123,12 @@ function parseNudges(node, stack) {
116
123
  }
117
124
  }
118
125
  }
119
- if (is$1(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
120
- if (!is$1(sortItem, "SortBy")) continue;
126
+ if (is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
127
+ if (!is(sortItem, "SortBy")) continue;
121
128
  const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
122
129
  const sortNulls = sortItem.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT";
123
130
  if (sortDir === "SORTBY_DESC" && sortNulls === "SORTBY_NULLS_DEFAULT") {
124
- if (sortItem.SortBy.node && is$1(sortItem.SortBy.node, "ColumnRef")) {
131
+ if (sortItem.SortBy.node && is(sortItem.SortBy.node, "ColumnRef")) {
125
132
  const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);
126
133
  if (!(sortColumnName !== null && whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName))) nudges.push({
127
134
  kind: "NULLS_FIRST_IN_DESC_ORDER",
@@ -132,8 +139,8 @@ function parseNudges(node, stack) {
132
139
  }
133
140
  }
134
141
  }
135
- if (is$1(node, "A_Expr")) {
136
- if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is$1(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>")) {
142
+ if (is(node, "A_Expr")) {
143
+ if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>")) {
137
144
  const leftIsNull = isNullConstant(node.A_Expr.lexpr);
138
145
  const rightIsNull = isNullConstant(node.A_Expr.rexpr);
139
146
  if (leftIsNull || rightIsNull) nudges.push({
@@ -147,7 +154,7 @@ function parseNudges(node, stack) {
147
154
  const patternString = getStringConstantValue(node.A_Expr.rexpr);
148
155
  if (patternString && patternString.startsWith("%")) {
149
156
  let stringNode;
150
- if (is$1(node.A_Expr.rexpr, "A_Const")) stringNode = node.A_Expr.rexpr.A_Const;
157
+ if (is(node.A_Expr.rexpr, "A_Const")) stringNode = node.A_Expr.rexpr.A_Const;
151
158
  nudges.push({
152
159
  kind: "AVOID_LEADING_WILDCARD_LIKE",
153
160
  severity: "WARNING",
@@ -157,48 +164,52 @@ function parseNudges(node, stack) {
157
164
  }
158
165
  }
159
166
  }
160
- if (is$1(node, "SelectStmt") && node.SelectStmt.sortClause) {
161
- 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({
167
+ if (is(node, "SelectStmt") && node.SelectStmt.sortClause) {
168
+ for (const sortItem of node.SelectStmt.sortClause) if (is(sortItem, "SortBy") && sortItem.SortBy.node && is(sortItem.SortBy.node, "FuncCall") && sortItem.SortBy.node.FuncCall.funcname?.some((name) => is(name, "String") && name.String.sval === "random")) nudges.push({
162
169
  kind: "AVOID_ORDER_BY_RANDOM",
163
170
  severity: "WARNING",
164
171
  message: "Avoid using ORDER BY random()",
165
172
  location: sortItem.SortBy.node.FuncCall.location
166
173
  });
167
174
  }
168
- if (is$1(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
175
+ if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
169
176
  kind: "AVOID_DISTINCT_WITHOUT_REASON",
170
177
  severity: "WARNING",
171
178
  message: "Avoid using DISTINCT without a reason"
172
179
  });
173
- if (is$1(node, "JoinExpr")) {
174
- if (!node.JoinExpr.quals) nudges.push({
180
+ if (is(node, "JoinExpr")) {
181
+ if (node.JoinExpr.quals) nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));
182
+ else if (!node.JoinExpr.usingClause) nudges.push({
175
183
  kind: "MISSING_JOIN_CONDITION",
176
184
  severity: "WARNING",
177
185
  message: "Missing JOIN condition"
178
186
  });
179
187
  }
180
- if (is$1(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
181
- const tables = node.SelectStmt.fromClause.filter((item) => is$1(item, "RangeVar"));
182
- if (tables.length > 1) nudges.push({
183
- kind: "MISSING_JOIN_CONDITION",
184
- severity: "WARNING",
185
- message: "Missing JOIN condition",
186
- location: tables[1].RangeVar.location
187
- });
188
+ if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
189
+ const tables = node.SelectStmt.fromClause.filter((item) => is(item, "RangeVar"));
190
+ if (tables.length > 1) {
191
+ const tableNames = new Set(tables.map((t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || ""));
192
+ if (!whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)) nudges.push({
193
+ kind: "MISSING_JOIN_CONDITION",
194
+ severity: "WARNING",
195
+ message: "Missing JOIN condition",
196
+ location: tables[1].RangeVar.location
197
+ });
198
+ }
188
199
  }
189
- if (is$1(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
190
- if (countBoolOrConditions(node) >= 3) nudges.push({
200
+ if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
201
+ if (countBoolOrConditions(node) >= 3 && allOrBranchesReferenceSameColumn(node)) nudges.push({
191
202
  kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
192
203
  severity: "WARNING",
193
204
  message: "Consider using IN instead of many ORs",
194
205
  location: node.BoolExpr.location
195
206
  });
196
207
  }
197
- if (is$1(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
208
+ if (is(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
198
209
  const args = node.BoolExpr.args;
199
210
  if (args && args.length === 1) {
200
211
  const inner = args[0];
201
- if (isANode$1(inner) && is$1(inner, "SubLink") && inner.SubLink.subLinkType === "ANY_SUBLINK") nudges.push({
212
+ if (isANode(inner) && is(inner, "SubLink") && inner.SubLink.subLinkType === "ANY_SUBLINK") nudges.push({
202
213
  kind: "PREFER_NOT_EXISTS_OVER_NOT_IN",
203
214
  severity: "WARNING",
204
215
  message: "Prefer NOT EXISTS over NOT IN (SELECT ...)",
@@ -206,21 +217,21 @@ function parseNudges(node, stack) {
206
217
  });
207
218
  }
208
219
  }
209
- if (is$1(node, "FuncCall")) {
220
+ if (is(node, "FuncCall")) {
210
221
  const funcName = node.FuncCall.funcname;
211
- 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({
222
+ if (funcName && funcName.length === 1 && is(funcName[0], "String") && funcName[0].String.sval === "count" && node.FuncCall.args && !node.FuncCall.agg_star && !node.FuncCall.agg_distinct) nudges.push({
212
223
  kind: "PREFER_COUNT_STAR_OVER_COUNT_COLUMN",
213
224
  severity: "INFO",
214
225
  message: "Prefer COUNT(*) over COUNT(column) or COUNT(1) — COUNT(*) counts rows without checking for NULLs. If you need to count non-NULL values, COUNT(column) is correct.",
215
226
  location: node.FuncCall.location
216
227
  });
217
228
  }
218
- if (is$1(node, "SelectStmt") && node.SelectStmt.havingClause) {
229
+ if (is(node, "SelectStmt") && node.SelectStmt.havingClause) {
219
230
  if (!containsAggregate(node.SelectStmt.havingClause)) {
220
231
  const having = node.SelectStmt.havingClause;
221
232
  let location;
222
- if (is$1(having, "A_Expr")) location = having.A_Expr.location;
223
- else if (is$1(having, "BoolExpr")) location = having.BoolExpr.location;
233
+ if (is(having, "A_Expr")) location = having.A_Expr.location;
234
+ else if (is(having, "BoolExpr")) location = having.BoolExpr.location;
224
235
  nudges.push({
225
236
  kind: "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES",
226
237
  severity: "INFO",
@@ -229,7 +240,7 @@ function parseNudges(node, stack) {
229
240
  });
230
241
  }
231
242
  }
232
- if (is$1(node, "FuncCall")) {
243
+ if (is(node, "FuncCall")) {
233
244
  if (stack.some((item) => item === "whereClause") && node.FuncCall.args) {
234
245
  const name = getFuncName(node);
235
246
  if (name && JSONB_SET_RETURNING_FUNCTIONS.has(name) && containsColumnRef(node.FuncCall.args)) nudges.push({
@@ -240,11 +251,11 @@ function parseNudges(node, stack) {
240
251
  });
241
252
  }
242
253
  }
243
- if (is$1(node, "A_Expr")) {
254
+ if (is(node, "A_Expr")) {
244
255
  if (node.A_Expr.kind === "AEXPR_IN") {
245
256
  let list;
246
- if (node.A_Expr.lexpr && is$1(node.A_Expr.lexpr, "List")) list = node.A_Expr.lexpr.List;
247
- else if (node.A_Expr.rexpr && is$1(node.A_Expr.rexpr, "List")) list = node.A_Expr.rexpr.List;
257
+ if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "List")) list = node.A_Expr.lexpr.List;
258
+ else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "List")) list = node.A_Expr.rexpr.List;
248
259
  if (list?.items && list.items.length >= 10) nudges.push({
249
260
  kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
250
261
  message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
@@ -253,8 +264,8 @@ function parseNudges(node, stack) {
253
264
  });
254
265
  }
255
266
  }
256
- if (is$1(node, "FuncCall")) {
257
- const funcname = node.FuncCall.funcname?.[0] && is$1(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
267
+ if (is(node, "FuncCall")) {
268
+ const funcname = node.FuncCall.funcname?.[0] && is(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
258
269
  if (funcname && [
259
270
  "sum",
260
271
  "count",
@@ -263,11 +274,11 @@ function parseNudges(node, stack) {
263
274
  "max"
264
275
  ].includes(funcname.toLowerCase())) {
265
276
  const firstArg = node.FuncCall.args?.[0];
266
- if (firstArg && isANode$1(firstArg) && is$1(firstArg, "CaseExpr")) {
277
+ if (firstArg && isANode(firstArg) && is(firstArg, "CaseExpr")) {
267
278
  const caseExpr = firstArg.CaseExpr;
268
279
  if (caseExpr.args && caseExpr.args.length === 1) {
269
280
  const defresult = caseExpr.defresult;
270
- 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({
281
+ if (!defresult || isANode(defresult) && is(defresult, "A_Const") && (defresult.A_Const.isnull !== void 0 || defresult.A_Const.ival !== void 0 && (defresult.A_Const.ival.ival === 0 || defresult.A_Const.ival.ival === void 0))) nudges.push({
271
282
  kind: "PREFER_FILTER_OVER_CASE_IN_AGGREGATE",
272
283
  severity: "INFO",
273
284
  message: "Use FILTER (WHERE ...) instead of CASE inside aggregate functions",
@@ -277,14 +288,14 @@ function parseNudges(node, stack) {
277
288
  }
278
289
  }
279
290
  }
280
- if (is$1(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
291
+ if (is(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
281
292
  kind: "PREFER_UNION_ALL_OVER_UNION",
282
293
  severity: "INFO",
283
294
  message: "UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed"
284
295
  });
285
- if (is$1(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
296
+ if (is(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
286
297
  const opNode = node.A_Expr.name[0];
287
- const op = is$1(opNode, "String") ? opNode.String.sval : null;
298
+ const op = is(opNode, "String") ? opNode.String.sval : null;
288
299
  if (op && isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)) nudges.push({
289
300
  kind: "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK",
290
301
  severity: "INFO",
@@ -299,32 +310,32 @@ function containsColumnRef(args) {
299
310
  return false;
300
311
  }
301
312
  function hasColumnRefInNode(node) {
302
- if (isANode$1(node) && is$1(node, "ColumnRef")) return true;
313
+ if (isANode(node) && is(node, "ColumnRef")) return true;
303
314
  if (typeof node !== "object" || node === null) return false;
304
315
  if (Array.isArray(node)) return node.some((item) => hasColumnRefInNode(item));
305
- if (isANode$1(node)) return hasColumnRefInNode(node[Object.keys(node)[0]]);
316
+ if (isANode(node)) return hasColumnRefInNode(node[Object.keys(node)[0]]);
306
317
  for (const child of Object.values(node)) if (hasColumnRefInNode(child)) return true;
307
318
  return false;
308
319
  }
309
320
  function hasActualTablesInJoin(joinExpr) {
310
- if (joinExpr.JoinExpr.larg && is$1(joinExpr.JoinExpr.larg, "RangeVar")) return true;
311
- if (joinExpr.JoinExpr.larg && is$1(joinExpr.JoinExpr.larg, "JoinExpr")) {
321
+ if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) return true;
322
+ if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
312
323
  if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) return true;
313
324
  }
314
- if (joinExpr.JoinExpr.rarg && is$1(joinExpr.JoinExpr.rarg, "RangeVar")) return true;
315
- if (joinExpr.JoinExpr.rarg && is$1(joinExpr.JoinExpr.rarg, "JoinExpr")) {
325
+ if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) return true;
326
+ if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
316
327
  if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) return true;
317
328
  }
318
329
  return false;
319
330
  }
320
331
  function isNullConstant(node) {
321
332
  if (!node || typeof node !== "object") return false;
322
- if (isANode$1(node) && is$1(node, "A_Const")) return node.A_Const.isnull !== void 0;
333
+ if (isANode(node) && is(node, "A_Const")) return node.A_Const.isnull !== void 0;
323
334
  return false;
324
335
  }
325
336
  function getStringConstantValue(node) {
326
337
  if (!node || typeof node !== "object") return null;
327
- if (isANode$1(node) && is$1(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
338
+ if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
328
339
  return null;
329
340
  }
330
341
  const JSONB_SET_RETURNING_FUNCTIONS = new Set([
@@ -341,21 +352,21 @@ function getFuncName(node) {
341
352
  const names = node.FuncCall.funcname;
342
353
  if (!names || names.length === 0) return null;
343
354
  const last = names[names.length - 1];
344
- if (isANode$1(last) && is$1(last, "String") && last.String.sval) return last.String.sval;
355
+ if (isANode(last) && is(last, "String") && last.String.sval) return last.String.sval;
345
356
  return null;
346
357
  }
347
358
  function getLastColumnRefField(columnRef) {
348
359
  const fields = columnRef.ColumnRef.fields;
349
360
  if (!fields || fields.length === 0) return null;
350
361
  const lastField = fields[fields.length - 1];
351
- if (isANode$1(lastField) && is$1(lastField, "String")) return lastField.String.sval || null;
362
+ if (isANode(lastField) && is(lastField, "String")) return lastField.String.sval || null;
352
363
  return null;
353
364
  }
354
365
  function whereHasIsNotNull(whereClause, columnName) {
355
366
  if (!whereClause) return false;
356
367
  let found = false;
357
368
  Walker.shallowMatch(whereClause, "NullTest", (node) => {
358
- if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is$1(node.NullTest.arg, "ColumnRef")) {
369
+ if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is(node.NullTest.arg, "ColumnRef")) {
359
370
  if (getLastColumnRefField(node.NullTest.arg) === columnName) found = true;
360
371
  }
361
372
  });
@@ -376,41 +387,106 @@ const AGGREGATE_FUNCTIONS = new Set([
376
387
  function containsAggregate(node) {
377
388
  if (!node || typeof node !== "object") return false;
378
389
  if (Array.isArray(node)) return node.some(containsAggregate);
379
- if (isANode$1(node) && is$1(node, "FuncCall")) {
390
+ if (isANode(node) && is(node, "FuncCall")) {
380
391
  const funcname = node.FuncCall.funcname;
381
392
  if (funcname) {
382
- for (const f of funcname) if (isANode$1(f) && is$1(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
393
+ for (const f of funcname) if (isANode(f) && is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
383
394
  }
384
395
  }
385
- if (isANode$1(node)) return containsAggregate(node[Object.keys(node)[0]]);
396
+ if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
386
397
  for (const child of Object.values(node)) if (containsAggregate(child)) return true;
387
398
  return false;
388
399
  }
389
400
  function countBoolOrConditions(node) {
390
401
  if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
391
402
  let count = 0;
392
- for (const arg of node.BoolExpr.args) if (isANode$1(arg) && is$1(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") count += countBoolOrConditions(arg);
403
+ for (const arg of node.BoolExpr.args) if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") count += countBoolOrConditions(arg);
393
404
  else count += 1;
394
405
  return count;
395
406
  }
407
+ /**
408
+ * Check whether every leaf of a top-level OR expression references the same
409
+ * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).
410
+ * Returns false when ORs span different columns — IN rewrite doesn't apply.
411
+ */
412
+ function allOrBranchesReferenceSameColumn(node) {
413
+ const columns = collectOrLeafColumns(node);
414
+ if (columns.length === 0) return false;
415
+ return columns.every((col) => col === columns[0]);
416
+ }
417
+ function collectOrLeafColumns(node) {
418
+ if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return [];
419
+ const columns = [];
420
+ for (const arg of node.BoolExpr.args) {
421
+ if (!isANode(arg)) continue;
422
+ if (is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") columns.push(...collectOrLeafColumns(arg));
423
+ else if (is(arg, "A_Expr")) {
424
+ const col = getLeftColumnKey(arg);
425
+ if (col) columns.push(col);
426
+ else return [];
427
+ } else return [];
428
+ }
429
+ return columns;
430
+ }
431
+ /**
432
+ * Get a string key for the left-hand column of an A_Expr equality comparison.
433
+ * For `t1.col = value` returns `"t1.col"`, for `col = value` returns `"col"`.
434
+ */
435
+ function getLeftColumnKey(expr) {
436
+ if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;
437
+ if (!is(expr.A_Expr.lexpr, "ColumnRef")) return null;
438
+ const fields = expr.A_Expr.lexpr.ColumnRef.fields;
439
+ if (!fields) return null;
440
+ return fields.filter((f) => isANode(f) && is(f, "String")).map((f) => f.String.sval).join(".");
441
+ }
442
+ /**
443
+ * Check if a WHERE clause contains an equality between columns from different
444
+ * tables (indicating an old-style implicit join condition).
445
+ */
446
+ function whereHasCrossTableEquality(whereClause, tableNames) {
447
+ if (!whereClause) return false;
448
+ if (isANode(whereClause) && is(whereClause, "A_Expr")) {
449
+ if (whereClause.A_Expr.kind === "AEXPR_OP" && whereClause.A_Expr.name?.some((n) => is(n, "String") && n.String.sval === "=")) {
450
+ const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);
451
+ const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);
452
+ if (leftTable && rightTable && leftTable !== rightTable && tableNames.has(leftTable) && tableNames.has(rightTable)) return true;
453
+ }
454
+ }
455
+ if (isANode(whereClause) && is(whereClause, "BoolExpr")) {
456
+ for (const arg of whereClause.BoolExpr.args ?? []) if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) return true;
457
+ }
458
+ return false;
459
+ }
460
+ /**
461
+ * Extract the table qualifier (first field) from a ColumnRef node.
462
+ * e.g. `t1.uid` → `"t1"`, `uid` → null
463
+ */
464
+ function getColumnRefTableQualifier(node) {
465
+ if (!node || !isANode(node) || !is(node, "ColumnRef")) return null;
466
+ const fields = node.ColumnRef.fields;
467
+ if (!fields || fields.length < 2) return null;
468
+ const first = fields[0];
469
+ if (isANode(first) && is(first, "String")) return first.String.sval || null;
470
+ return null;
471
+ }
396
472
  function isCountFuncCall(node) {
397
473
  if (!node || typeof node !== "object") return false;
398
- if (!isANode$1(node) || !is$1(node, "FuncCall")) return false;
474
+ if (!isANode(node) || !is(node, "FuncCall")) return false;
399
475
  const fc = node.FuncCall;
400
- if (!(fc.funcname?.some((n) => is$1(n, "String") && n.String.sval === "count") ?? false)) return false;
476
+ if (!(fc.funcname?.some((n) => is(n, "String") && n.String.sval === "count") ?? false)) return false;
401
477
  if (fc.agg_star) return true;
402
- if (fc.args && fc.args.length === 1 && isANode$1(fc.args[0]) && is$1(fc.args[0], "A_Const")) return true;
478
+ if (fc.args && fc.args.length === 1 && isANode(fc.args[0]) && is(fc.args[0], "A_Const")) return true;
403
479
  return false;
404
480
  }
405
481
  function isSubLinkWithCount(node) {
406
482
  if (!node || typeof node !== "object") return false;
407
- if (!isANode$1(node) || !is$1(node, "SubLink")) return false;
483
+ if (!isANode(node) || !is(node, "SubLink")) return false;
408
484
  const subselect = node.SubLink.subselect;
409
- if (!subselect || !isANode$1(subselect) || !is$1(subselect, "SelectStmt")) return false;
485
+ if (!subselect || !isANode(subselect) || !is(subselect, "SelectStmt")) return false;
410
486
  const targets = subselect.SelectStmt.targetList;
411
487
  if (!targets || targets.length !== 1) return false;
412
488
  const target = targets[0];
413
- if (!isANode$1(target) || !is$1(target, "ResTarget") || !target.ResTarget.val) return false;
489
+ if (!isANode(target) || !is(target, "ResTarget") || !target.ResTarget.val) return false;
414
490
  return isCountFuncCall(target.ResTarget.val);
415
491
  }
416
492
  function isCountExpression(node) {
@@ -418,7 +494,7 @@ function isCountExpression(node) {
418
494
  }
419
495
  function getIntegerConstantValue(node) {
420
496
  if (!node || typeof node !== "object") return null;
421
- if (!isANode$1(node) || !is$1(node, "A_Const")) return null;
497
+ if (!isANode(node) || !is(node, "A_Const")) return null;
422
498
  if (node.A_Const.ival === void 0) return null;
423
499
  return node.A_Const.ival.ival ?? 0;
424
500
  }
@@ -441,9 +517,8 @@ function isExistenceCheckPattern(lexpr, rexpr, op) {
441
517
  }
442
518
  return false;
443
519
  }
444
-
445
520
  //#endregion
446
- //#region \0@oxc-project+runtime@0.112.0/helpers/typeof.js
521
+ //#region \0@oxc-project+runtime@0.122.0/helpers/typeof.js
447
522
  function _typeof(o) {
448
523
  "@babel/helpers - typeof";
449
524
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -452,9 +527,8 @@ function _typeof(o) {
452
527
  return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
453
528
  }, _typeof(o);
454
529
  }
455
-
456
530
  //#endregion
457
- //#region \0@oxc-project+runtime@0.112.0/helpers/toPrimitive.js
531
+ //#region \0@oxc-project+runtime@0.122.0/helpers/toPrimitive.js
458
532
  function toPrimitive(t, r) {
459
533
  if ("object" != _typeof(t) || !t) return t;
460
534
  var e = t[Symbol.toPrimitive];
@@ -465,16 +539,14 @@ function toPrimitive(t, r) {
465
539
  }
466
540
  return ("string" === r ? String : Number)(t);
467
541
  }
468
-
469
542
  //#endregion
470
- //#region \0@oxc-project+runtime@0.112.0/helpers/toPropertyKey.js
543
+ //#region \0@oxc-project+runtime@0.122.0/helpers/toPropertyKey.js
471
544
  function toPropertyKey(t) {
472
545
  var i = toPrimitive(t, "string");
473
546
  return "symbol" == _typeof(i) ? i : i + "";
474
547
  }
475
-
476
548
  //#endregion
477
- //#region \0@oxc-project+runtime@0.112.0/helpers/defineProperty.js
549
+ //#region \0@oxc-project+runtime@0.122.0/helpers/defineProperty.js
478
550
  function _defineProperty(e, r, t) {
479
551
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
480
552
  value: t,
@@ -483,7 +555,6 @@ function _defineProperty(e, r, t) {
483
555
  writable: !0
484
556
  }) : e[r] = t, e;
485
557
  }
486
-
487
558
  //#endregion
488
559
  //#region src/sql/walker.ts
489
560
  const JSONB_EXTRACTION_OPS = new Set(["->", "->>"]);
@@ -730,17 +801,6 @@ var Walker = class Walker {
730
801
  } else for (const [key, child] of Object.entries(node)) Walker.doTraverse(child, [...stack, key], callback);
731
802
  }
732
803
  };
733
- function is(node, kind) {
734
- return kind in node;
735
- }
736
- function getNodeKind(node) {
737
- return Object.keys(node)[0];
738
- }
739
- function isANode(node) {
740
- if (typeof node !== "object" || node === null) return false;
741
- const keys = Object.keys(node);
742
- return keys.length === 1 && /^[A-Z]/.test(keys[0]);
743
- }
744
804
  /**
745
805
  * Given an operand of a comparison (e.g. the left side of `=`), check whether
746
806
  * it is a JSONB path extraction expression such as `data->>'email'` or
@@ -792,7 +852,6 @@ function stripTableQualifiers(node) {
792
852
  for (const value of Object.values(node)) if (Array.isArray(value)) for (const item of value) stripTableQualifiers(item);
793
853
  else if (typeof value === "object" && value !== null) stripTableQualifiers(value);
794
854
  }
795
-
796
855
  //#endregion
797
856
  //#region src/sql/analyzer.ts
798
857
  const ignoredIdentifier = "__qd_placeholder";
@@ -1018,10 +1077,17 @@ var Analyzer = class {
1018
1077
  };
1019
1078
  }
1020
1079
  };
1021
-
1022
1080
  //#endregion
1023
1081
  //#region src/sql/database.ts
1024
1082
  const PostgresVersion = zod.z.string().brand("PostgresVersion");
1083
+ /** Zod schema for PostgresExplainStage — validates common fields, passes through variant-specific ones. */
1084
+ const PostgresExplainStageSchema = zod.z.lazy(() => zod.z.object({
1085
+ "Node Type": zod.z.string(),
1086
+ "Total Cost": zod.z.number(),
1087
+ "Plan Width": zod.z.number(),
1088
+ "Node Id": zod.z.number().optional(),
1089
+ Plans: zod.z.array(PostgresExplainStageSchema).optional()
1090
+ }).passthrough());
1025
1091
  /**
1026
1092
  * Drops a disabled index. Rollsback if it fails for any reason
1027
1093
  * @returns Did dropping the index succeed?
@@ -1038,7 +1104,6 @@ async function dropIndex(tx, index) {
1038
1104
  return false;
1039
1105
  }
1040
1106
  }
1041
-
1042
1107
  //#endregion
1043
1108
  //#region src/sql/indexes.ts
1044
1109
  function isIndexSupported(index) {
@@ -1051,7 +1116,6 @@ function isIndexSupported(index) {
1051
1116
  function isIndexProbablyDroppable(index) {
1052
1117
  return !index.is_primary && !index.is_unique;
1053
1118
  }
1054
-
1055
1119
  //#endregion
1056
1120
  //#region src/sql/builder.ts
1057
1121
  var PostgresQueryBuilder = class PostgresQueryBuilder {
@@ -1141,7 +1205,6 @@ var PostgresQueryBuilder = class PostgresQueryBuilder {
1141
1205
  return query;
1142
1206
  }
1143
1207
  };
1144
-
1145
1208
  //#endregion
1146
1209
  //#region src/sql/pg-identifier.ts
1147
1210
  /**
@@ -1353,7 +1416,6 @@ _defineProperty(PgIdentifier, "reservedKeywords", new Set([
1353
1416
  "xmlserialize",
1354
1417
  "xmltable"
1355
1418
  ]));
1356
-
1357
1419
  //#endregion
1358
1420
  //#region src/sql/permutations.ts
1359
1421
  /**
@@ -1378,7 +1440,6 @@ function permutationsWithDescendingLength(arr) {
1378
1440
  collected.sort((a, b) => b.length - a.length);
1379
1441
  return collected;
1380
1442
  }
1381
-
1382
1443
  //#endregion
1383
1444
  //#region src/optimizer/genalgo.ts
1384
1445
  var IndexOptimizer = class IndexOptimizer {
@@ -1717,7 +1778,6 @@ var RollbackError = class {
1717
1778
  };
1718
1779
  const PROCEED = Symbol("PROCEED");
1719
1780
  const SKIP = Symbol("SKIP");
1720
-
1721
1781
  //#endregion
1722
1782
  //#region src/optimizer/statistics.ts
1723
1783
  const StatisticsSource = zod.z.union([zod.z.object({
@@ -2357,7 +2417,6 @@ _defineProperty(Statistics, "defaultStatsMode", Object.freeze({
2357
2417
  reltuples: DEFAULT_RELTUPLES,
2358
2418
  relpages: DEFAULT_RELPAGES
2359
2419
  }));
2360
-
2361
2420
  //#endregion
2362
2421
  //#region src/optimizer/pss-rewriter.ts
2363
2422
  /**
@@ -2384,7 +2443,6 @@ var PssRewriter = class {
2384
2443
  });
2385
2444
  }
2386
2445
  };
2387
-
2388
2446
  //#endregion
2389
2447
  exports.Analyzer = Analyzer;
2390
2448
  exports.ExportedStats = ExportedStats;
@@ -2395,6 +2453,7 @@ exports.ExportedStatsV1 = ExportedStatsV1;
2395
2453
  exports.IndexOptimizer = IndexOptimizer;
2396
2454
  exports.PROCEED = PROCEED;
2397
2455
  exports.PgIdentifier = PgIdentifier;
2456
+ exports.PostgresExplainStageSchema = PostgresExplainStageSchema;
2398
2457
  exports.PostgresQueryBuilder = PostgresQueryBuilder;
2399
2458
  exports.PostgresVersion = PostgresVersion;
2400
2459
  exports.PssRewriter = PssRewriter;
@@ -2407,4 +2466,5 @@ exports.ignoredIdentifier = ignoredIdentifier;
2407
2466
  exports.isIndexProbablyDroppable = isIndexProbablyDroppable;
2408
2467
  exports.isIndexSupported = isIndexSupported;
2409
2468
  exports.parseNudges = parseNudges;
2469
+
2410
2470
  //# sourceMappingURL=index.cjs.map