@query-doctor/core 0.4.1-rc.3 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -42,70 +42,6 @@ function isANode$1(node) {
42
42
  const keys = Object.keys(node);
43
43
  return keys.length === 1 && /^[A-Z]/.test(keys[0]);
44
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
45
  const findFuncCallsOnColumns = (whereClause) => {
110
46
  const nudges = [];
111
47
  Walker.shallowMatch(whereClause, "FuncCall", (node) => {
@@ -116,77 +52,6 @@ const findFuncCallsOnColumns = (whereClause) => {
116
52
  location: node.FuncCall.location
117
53
  });
118
54
  });
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
55
  return nudges;
191
56
  };
192
57
  /**
@@ -196,11 +61,7 @@ const findArithmeticExprsOnColumns = (whereClause) => {
196
61
  function parseNudges(node, stack) {
197
62
  const nudges = [];
198
63
  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
- }
64
+ if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
204
65
  const star = node.SelectStmt.targetList?.find((target) => {
205
66
  if (!(is$1(target, "ResTarget") && target.ResTarget.val && is$1(target.ResTarget.val, "ColumnRef"))) return false;
206
67
  const fields = target.ResTarget.val.ColumnRef.fields;
@@ -217,12 +78,6 @@ function parseNudges(node, stack) {
217
78
  location: star.ResTarget.location
218
79
  });
219
80
  }
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
81
  }
227
82
  if (is$1(node, "SelectStmt")) {
228
83
  if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
@@ -247,44 +102,8 @@ function parseNudges(node, stack) {
247
102
  }
248
103
  }
249
104
  }
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
105
  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) {
106
+ if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is$1(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>")) {
288
107
  const leftIsNull = isNullConstant(node.A_Expr.lexpr);
289
108
  const rightIsNull = isNullConstant(node.A_Expr.rexpr);
290
109
  if (leftIsNull || rightIsNull) nudges.push({
@@ -294,8 +113,7 @@ function parseNudges(node, stack) {
294
113
  location: node.A_Expr.location
295
114
  });
296
115
  }
297
- const isLikeOp = node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE";
298
- if (isLikeOp && node.A_Expr.rexpr) {
116
+ if ((node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE") && node.A_Expr.rexpr) {
299
117
  const patternString = getStringConstantValue(node.A_Expr.rexpr);
300
118
  if (patternString && patternString.startsWith("%")) {
301
119
  let stringNode;
@@ -303,44 +121,17 @@ function parseNudges(node, stack) {
303
121
  nudges.push({
304
122
  kind: "AVOID_LEADING_WILDCARD_LIKE",
305
123
  severity: "WARNING",
306
- message: "Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search",
124
+ message: "Avoid using LIKE with leading wildcards",
307
125
  location: stringNode?.location
308
126
  });
309
127
  }
310
128
  }
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
129
  }
329
130
  if (is$1(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
330
131
  kind: "AVOID_DISTINCT_WITHOUT_REASON",
331
132
  severity: "WARNING",
332
133
  message: "Avoid using DISTINCT without a reason"
333
134
  });
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
135
  if (is$1(node, "JoinExpr")) {
345
136
  if (!node.JoinExpr.quals) nudges.push({
346
137
  kind: "MISSING_JOIN_CONDITION",
@@ -377,93 +168,6 @@ function parseNudges(node, stack) {
377
168
  });
378
169
  }
379
170
  }
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
171
  if (is$1(node, "A_Expr")) {
468
172
  if (node.A_Expr.kind === "AEXPR_IN") {
469
173
  let list;
@@ -477,36 +181,6 @@ function parseNudges(node, stack) {
477
181
  });
478
182
  }
479
183
  }
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
184
  return nudges;
511
185
  }
512
186
  function containsColumnRef(args) {
@@ -542,142 +216,6 @@ function getStringConstantValue(node) {
542
216
  if (isANode$1(node) && is$1(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
543
217
  return null;
544
218
  }
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;
680
- }
681
219
  function countBoolOrConditions(node) {
682
220
  if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
683
221
  let count = 0;
@@ -685,28 +223,6 @@ function countBoolOrConditions(node) {
685
223
  else count += 1;
686
224
  return count;
687
225
  }
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;
709
- }
710
226
 
711
227
  //#endregion
712
228
  //#region \0@oxc-project+runtime@0.112.0/helpers/typeof.js
@@ -837,11 +353,6 @@ var Walker = class Walker {
837
353
  if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "ColumnRef")) this.add(node.A_Expr.lexpr, { jsonbOperator });
838
354
  if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "ColumnRef")) this.add(node.A_Expr.rexpr, { jsonbOperator });
839
355
  }
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
356
  }
846
357
  if (is(node, "ColumnRef")) {
847
358
  for (let i = 0; i < stack.length; i++) {
@@ -925,7 +436,6 @@ var Walker = class Walker {
925
436
  if (options?.sort) ref.sort = options.sort;
926
437
  if (options?.where) ref.where = options.where;
927
438
  if (options?.jsonbOperator) ref.jsonbOperator = options.jsonbOperator;
928
- if (options?.gistOperator) ref.gistOperator = options.gistOperator;
929
439
  this.highlights.push(ref);
930
440
  }
931
441
  /**
@@ -1094,7 +604,6 @@ var Analyzer = class {
1094
604
  if (colReference.sort) index.sort = colReference.sort;
1095
605
  if (colReference.where) index.where = colReference.where;
1096
606
  if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
1097
- if (colReference.gistOperator) index.gistOperator = colReference.gistOperator;
1098
607
  addIndex(index);
1099
608
  }
1100
609
  } else if (tableReference) {
@@ -1111,7 +620,6 @@ var Analyzer = class {
1111
620
  if (colReference.sort) index.sort = colReference.sort;
1112
621
  if (colReference.where) index.where = colReference.where;
1113
622
  if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
1114
- if (colReference.gistOperator) index.gistOperator = colReference.gistOperator;
1115
623
  addIndex(index);
1116
624
  }
1117
625
  } else if (fullReference) {
@@ -1124,7 +632,6 @@ var Analyzer = class {
1124
632
  if (colReference.sort) index.sort = colReference.sort;
1125
633
  if (colReference.where) index.where = colReference.where;
1126
634
  if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
1127
- if (colReference.gistOperator) index.gistOperator = colReference.gistOperator;
1128
635
  addIndex(index);
1129
636
  } else {
1130
637
  console.error("Column reference has too many parts. The query is malformed", colReference);
@@ -1236,7 +743,7 @@ async function dropIndex(tx, index) {
1236
743
  //#endregion
1237
744
  //#region src/sql/indexes.ts
1238
745
  function isIndexSupported(index) {
1239
- return index.index_type === "btree" || index.index_type === "gin" || index.index_type === "gist";
746
+ return index.index_type === "btree" || index.index_type === "gin";
1240
747
  }
1241
748
  /**
1242
749
  * Doesn't necessarily decide whether the index can be dropped but can be
@@ -1670,9 +1177,8 @@ var IndexOptimizer = class IndexOptimizer {
1670
1177
  * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
1671
1178
  **/
1672
1179
  indexesToCreate(rootCandidates) {
1673
- const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator && !c.gistOperator);
1180
+ const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
1674
1181
  const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
1675
- const gistCandidates = rootCandidates.filter((c) => c.gistOperator);
1676
1182
  const nextStage = [];
1677
1183
  const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
1678
1184
  for (const permutation of permutedIndexes.values()) {
@@ -1726,32 +1232,6 @@ var IndexOptimizer = class IndexOptimizer {
1726
1232
  opclass
1727
1233
  });
1728
1234
  }
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
1235
  return nextStage;
1756
1236
  }
1757
1237
  toDefinition({ schema, table, columns }) {
@@ -1782,12 +1262,6 @@ var IndexOptimizer = class IndexOptimizer {
1782
1262
  const opclassSuffix = opclass ? ` ${opclass}` : "";
1783
1263
  return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
1784
1264
  }
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
1265
  groupGinCandidatesByColumn(candidates) {
1792
1266
  const groups = /* @__PURE__ */ new Map();
1793
1267
  for (const c of candidates) {
@@ -1808,22 +1282,6 @@ var IndexOptimizer = class IndexOptimizer {
1808
1282
  ginIndexAlreadyExists(table, column) {
1809
1283
  return this.existingIndexes.find((index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column));
1810
1284
  }
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
1285
  /**
1828
1286
  * Drop indexes that can be dropped. Ignore the ones that can't
1829
1287
  */
@@ -2618,7 +2076,5 @@ exports.dropIndex = dropIndex;
2618
2076
  exports.ignoredIdentifier = ignoredIdentifier;
2619
2077
  exports.isIndexProbablyDroppable = isIndexProbablyDroppable;
2620
2078
  exports.isIndexSupported = isIndexSupported;
2621
- exports.parseMajorVersion = parseMajorVersion;
2622
2079
  exports.parseNudges = parseNudges;
2623
- exports.parseVersionNudges = parseVersionNudges;
2624
2080
  //# sourceMappingURL=index.cjs.map