@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.mjs CHANGED
@@ -13,70 +13,6 @@ function isANode$1(node) {
13
13
  const keys = Object.keys(node);
14
14
  return keys.length === 1 && /^[A-Z]/.test(keys[0]);
15
15
  }
16
- /** Operators that require a GiST index for efficient execution */
17
- const GIST_OPERATORS = new Set(["&&", "-|-"]);
18
- /** PostGIS / spatial functions that require a GiST index */
19
- const SPATIAL_FUNCTIONS = new Set([
20
- "st_intersects",
21
- "st_contains",
22
- "st_within",
23
- "st_dwithin",
24
- "st_covers",
25
- "st_coveredby",
26
- "st_crosses",
27
- "st_overlaps",
28
- "st_touches"
29
- ]);
30
- const JSONB_EXTRACTION_OPS = new Set([
31
- "->>",
32
- "->",
33
- "#>>",
34
- "#>"
35
- ]);
36
- function getAExprOpName(node) {
37
- const nameNode = node.A_Expr.name?.[0];
38
- if (nameNode && is$1(nameNode, "String") && nameNode.String.sval) return nameNode.String.sval;
39
- return null;
40
- }
41
- /**
42
- * Check if a node is or contains a JSONB extraction expression,
43
- * unwrapping through TypeCast. Returns the extraction A_Expr if found.
44
- */
45
- function findExtractionExpr(node) {
46
- if (is$1(node, "A_Expr")) {
47
- const op = getAExprOpName(node);
48
- if (op && JSONB_EXTRACTION_OPS.has(op)) {
49
- if (node.A_Expr.lexpr && hasColumnRefInNode(node.A_Expr.lexpr)) return node;
50
- return null;
51
- }
52
- }
53
- if (is$1(node, "TypeCast") && node.TypeCast.arg) return findExtractionExpr(node.TypeCast.arg);
54
- return null;
55
- }
56
- const ARITHMETIC_OPERATORS = new Set([
57
- "+",
58
- "-",
59
- "*",
60
- "/",
61
- "%"
62
- ]);
63
- const isArithmeticExpr = (node) => {
64
- if (!node || typeof node !== "object") return false;
65
- if (!isANode$1(node) || !is$1(node, "A_Expr")) return false;
66
- if (node.A_Expr.kind !== "AEXPR_OP" || !node.A_Expr.name?.length) return false;
67
- const opNode = node.A_Expr.name[0];
68
- if (!is$1(opNode, "String") || !opNode.String.sval) return false;
69
- return ARITHMETIC_OPERATORS.has(opNode.String.sval);
70
- };
71
- const COMPARISON_OPERATORS = new Set([
72
- "=",
73
- "<",
74
- ">",
75
- "<=",
76
- ">=",
77
- "<>",
78
- "!="
79
- ]);
80
16
  const findFuncCallsOnColumns = (whereClause) => {
81
17
  const nudges = [];
82
18
  Walker.shallowMatch(whereClause, "FuncCall", (node) => {
@@ -87,77 +23,6 @@ const findFuncCallsOnColumns = (whereClause) => {
87
23
  location: node.FuncCall.location
88
24
  });
89
25
  });
90
- Walker.shallowMatch(whereClause, "CoalesceExpr", (node) => {
91
- if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) nudges.push({
92
- kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
93
- severity: "WARNING",
94
- message: "Avoid using functions on columns in WHERE clause",
95
- location: node.CoalesceExpr.location
96
- });
97
- });
98
- return nudges;
99
- };
100
- const findJsonbExtractionInWhere = (whereClause) => {
101
- const nudges = [];
102
- const seen = /* @__PURE__ */ new Set();
103
- function emit(node) {
104
- const extraction = findExtractionExpr(node);
105
- if (extraction && extraction.A_Expr.location !== void 0 && !seen.has(extraction.A_Expr.location)) {
106
- seen.add(extraction.A_Expr.location);
107
- nudges.push({
108
- kind: "CONSIDER_EXPRESSION_INDEX_FOR_JSONB_EXTRACTION",
109
- severity: "INFO",
110
- message: "Consider an expression B-tree index for JSONB path extraction",
111
- location: extraction.A_Expr.location
112
- });
113
- }
114
- }
115
- function walk(node) {
116
- if (is$1(node, "A_Expr")) {
117
- const op = getAExprOpName(node);
118
- if (op && JSONB_EXTRACTION_OPS.has(op)) return;
119
- if (node.A_Expr.lexpr) emit(node.A_Expr.lexpr);
120
- if (node.A_Expr.rexpr) emit(node.A_Expr.rexpr);
121
- return;
122
- }
123
- if (is$1(node, "BoolExpr") && node.BoolExpr.args) {
124
- for (const arg of node.BoolExpr.args) walk(arg);
125
- return;
126
- }
127
- if (is$1(node, "NullTest") && node.NullTest.arg) {
128
- emit(node.NullTest.arg);
129
- return;
130
- }
131
- if (is$1(node, "BooleanTest") && node.BooleanTest.arg) {
132
- emit(node.BooleanTest.arg);
133
- return;
134
- }
135
- if (is$1(node, "SubLink") && node.SubLink.testexpr) {
136
- emit(node.SubLink.testexpr);
137
- return;
138
- }
139
- }
140
- walk(whereClause);
141
- return nudges;
142
- };
143
- const findArithmeticExprsOnColumns = (whereClause) => {
144
- const nudges = [];
145
- Walker.shallowMatch(whereClause, "A_Expr", (node) => {
146
- if (node.A_Expr.kind !== "AEXPR_OP" || !node.A_Expr.name?.length) return;
147
- const opNode = node.A_Expr.name[0];
148
- if (!is$1(opNode, "String") || !opNode.String.sval) return;
149
- if (!COMPARISON_OPERATORS.has(opNode.String.sval)) return;
150
- const sides = [node.A_Expr.lexpr, node.A_Expr.rexpr];
151
- for (const side of sides) if (side && isArithmeticExpr(side) && hasColumnRefInNode(side)) {
152
- const expr = side;
153
- nudges.push({
154
- kind: "AVOID_EXPRESSIONS_ON_COLUMNS_IN_WHERE",
155
- severity: "WARNING",
156
- message: "Avoid using arithmetic expressions on columns in WHERE clause",
157
- location: expr.A_Expr.location
158
- });
159
- }
160
- });
161
26
  return nudges;
162
27
  };
163
28
  /**
@@ -167,11 +32,7 @@ const findArithmeticExprsOnColumns = (whereClause) => {
167
32
  function parseNudges(node, stack) {
168
33
  const nudges = [];
169
34
  if (is$1(node, "SelectStmt")) {
170
- if (node.SelectStmt.whereClause) {
171
- nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
172
- nudges.push(...findJsonbExtractionInWhere(node.SelectStmt.whereClause));
173
- nudges.push(...findArithmeticExprsOnColumns(node.SelectStmt.whereClause));
174
- }
35
+ if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
175
36
  const star = node.SelectStmt.targetList?.find((target) => {
176
37
  if (!(is$1(target, "ResTarget") && target.ResTarget.val && is$1(target.ResTarget.val, "ColumnRef"))) return false;
177
38
  const fields = target.ResTarget.val.ColumnRef.fields;
@@ -188,12 +49,6 @@ function parseNudges(node, stack) {
188
49
  location: star.ResTarget.location
189
50
  });
190
51
  }
191
- 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({
192
- kind: "AVOID_SCALAR_SUBQUERY_IN_SELECT",
193
- severity: "WARNING",
194
- message: "Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN",
195
- location: target.ResTarget.val.SubLink.location
196
- });
197
52
  }
198
53
  if (is$1(node, "SelectStmt")) {
199
54
  if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
@@ -218,44 +73,8 @@ function parseNudges(node, stack) {
218
73
  }
219
74
  }
220
75
  }
221
- if (is$1(node, "SelectStmt")) {
222
- if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr") && node.SelectStmt.sortClause && node.SelectStmt.whereClause) {
223
- const equalityColumns = collectEqualityConstrainedColumns(node.SelectStmt.whereClause);
224
- for (const sortItem of node.SelectStmt.sortClause) {
225
- if (!is$1(sortItem, "SortBy") || !sortItem.SortBy.node) continue;
226
- const sortNode = sortItem.SortBy.node;
227
- if (!is$1(sortNode, "ColumnRef") || !sortNode.ColumnRef.fields) continue;
228
- const lastField = sortNode.ColumnRef.fields[sortNode.ColumnRef.fields.length - 1];
229
- if (!is$1(lastField, "String")) continue;
230
- const sortCol = lastField.String.sval;
231
- if (sortCol && equalityColumns.has(sortCol)) nudges.push({
232
- kind: "NOOP_ORDER_BY",
233
- severity: "INFO",
234
- message: "ORDER BY column is constrained to a single value by WHERE clause",
235
- location: sortNode.ColumnRef.location
236
- });
237
- }
238
- }
239
- }
240
- if (is$1(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
241
- if (!is$1(sortItem, "SortBy")) continue;
242
- const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
243
- const sortNulls = sortItem.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT";
244
- if (sortDir === "SORTBY_DESC" && sortNulls === "SORTBY_NULLS_DEFAULT") {
245
- if (sortItem.SortBy.node && is$1(sortItem.SortBy.node, "ColumnRef")) {
246
- const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);
247
- if (!(sortColumnName !== null && whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName))) nudges.push({
248
- kind: "NULLS_FIRST_IN_DESC_ORDER",
249
- severity: "INFO",
250
- message: "ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end",
251
- location: sortItem.SortBy.node.ColumnRef.location
252
- });
253
- }
254
- }
255
- }
256
76
  if (is$1(node, "A_Expr")) {
257
- 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 === "<>");
258
- if (isEqualityOp) {
77
+ 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 === "<>")) {
259
78
  const leftIsNull = isNullConstant(node.A_Expr.lexpr);
260
79
  const rightIsNull = isNullConstant(node.A_Expr.rexpr);
261
80
  if (leftIsNull || rightIsNull) nudges.push({
@@ -265,8 +84,7 @@ function parseNudges(node, stack) {
265
84
  location: node.A_Expr.location
266
85
  });
267
86
  }
268
- const isLikeOp = node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE";
269
- if (isLikeOp && node.A_Expr.rexpr) {
87
+ if ((node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE") && node.A_Expr.rexpr) {
270
88
  const patternString = getStringConstantValue(node.A_Expr.rexpr);
271
89
  if (patternString && patternString.startsWith("%")) {
272
90
  let stringNode;
@@ -274,44 +92,17 @@ function parseNudges(node, stack) {
274
92
  nudges.push({
275
93
  kind: "AVOID_LEADING_WILDCARD_LIKE",
276
94
  severity: "WARNING",
277
- message: "Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search",
95
+ message: "Avoid using LIKE with leading wildcards",
278
96
  location: stringNode?.location
279
97
  });
280
98
  }
281
99
  }
282
- if (isEqualityOp === false && isLikeOp === false) {
283
- 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;
284
- if (gistOpName && GIST_OPERATORS.has(gistOpName)) nudges.push({
285
- kind: "CONSIDER_GIST_INDEX",
286
- severity: "INFO",
287
- message: `Operator "${gistOpName}" benefits from a GiST index for efficient execution`,
288
- location: node.A_Expr.location
289
- });
290
- }
291
- }
292
- if (is$1(node, "SelectStmt") && node.SelectStmt.sortClause) {
293
- 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({
294
- kind: "AVOID_ORDER_BY_RANDOM",
295
- severity: "WARNING",
296
- message: "Avoid using ORDER BY random()",
297
- location: sortItem.SortBy.node.FuncCall.location
298
- });
299
100
  }
300
101
  if (is$1(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
301
102
  kind: "AVOID_DISTINCT_WITHOUT_REASON",
302
103
  severity: "WARNING",
303
104
  message: "Avoid using DISTINCT without a reason"
304
105
  });
305
- if (is$1(node, "SelectStmt") && node.SelectStmt.limitOffset) {
306
- const offsetNode = node.SelectStmt.limitOffset;
307
- const location = isANode$1(offsetNode) && is$1(offsetNode, "A_Const") ? offsetNode.A_Const.location : void 0;
308
- nudges.push({
309
- kind: "AVOID_OFFSET_FOR_PAGINATION",
310
- severity: "INFO",
311
- message: "Avoid using OFFSET for pagination",
312
- location
313
- });
314
- }
315
106
  if (is$1(node, "JoinExpr")) {
316
107
  if (!node.JoinExpr.quals) nudges.push({
317
108
  kind: "MISSING_JOIN_CONDITION",
@@ -348,93 +139,6 @@ function parseNudges(node, stack) {
348
139
  });
349
140
  }
350
141
  }
351
- if (is$1(node, "SelectStmt") && node.SelectStmt.havingClause) {
352
- if (!containsAggregate(node.SelectStmt.havingClause)) {
353
- const having = node.SelectStmt.havingClause;
354
- let location;
355
- if (is$1(having, "A_Expr")) location = having.A_Expr.location;
356
- else if (is$1(having, "BoolExpr")) location = having.BoolExpr.location;
357
- nudges.push({
358
- kind: "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES",
359
- severity: "INFO",
360
- message: "Non-aggregate condition in HAVING should be in WHERE",
361
- location
362
- });
363
- }
364
- }
365
- if (is$1(node, "FuncCall")) {
366
- const funcName = node.FuncCall.funcname;
367
- 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({
368
- kind: "PREFER_COUNT_STAR_OVER_COUNT_COLUMN",
369
- severity: "INFO",
370
- 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.",
371
- location: node.FuncCall.location
372
- });
373
- }
374
- if (is$1(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
375
- const opNode = node.A_Expr.name[0];
376
- const op = is$1(opNode, "String") ? opNode.String.sval : null;
377
- if (op && isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)) nudges.push({
378
- kind: "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK",
379
- severity: "INFO",
380
- message: "Use EXISTS instead of COUNT for existence checks",
381
- location: node.A_Expr.location
382
- });
383
- }
384
- if (is$1(node, "FuncCall")) {
385
- const funcname = node.FuncCall.funcname?.[0] && is$1(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
386
- if (funcname && [
387
- "sum",
388
- "count",
389
- "avg",
390
- "min",
391
- "max"
392
- ].includes(funcname.toLowerCase())) {
393
- const firstArg = node.FuncCall.args?.[0];
394
- if (firstArg && isANode$1(firstArg) && is$1(firstArg, "CaseExpr")) {
395
- const caseExpr = firstArg.CaseExpr;
396
- if (caseExpr.args && caseExpr.args.length === 1) {
397
- const defresult = caseExpr.defresult;
398
- 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({
399
- kind: "PREFER_FILTER_OVER_CASE_IN_AGGREGATE",
400
- severity: "INFO",
401
- message: "Use FILTER (WHERE ...) instead of CASE inside aggregate functions",
402
- location: node.FuncCall.location
403
- });
404
- }
405
- }
406
- }
407
- }
408
- if (is$1(node, "FuncCall") && node.FuncCall.funcname) {
409
- const lastNameNode = node.FuncCall.funcname[node.FuncCall.funcname.length - 1];
410
- if (lastNameNode && is$1(lastNameNode, "String") && lastNameNode.String.sval) {
411
- const funcName = lastNameNode.String.sval.toLowerCase();
412
- if (SPATIAL_FUNCTIONS.has(funcName)) nudges.push({
413
- kind: "CONSIDER_GIST_INDEX",
414
- severity: "INFO",
415
- message: `Spatial function "${lastNameNode.String.sval}" benefits from a GiST index for efficient execution`,
416
- location: node.FuncCall.location
417
- });
418
- }
419
- }
420
- if (is$1(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
421
- const args = node.BoolExpr.args;
422
- if (args && args.length === 1) {
423
- const arg = args[0];
424
- if (isANode$1(arg) && is$1(arg, "SubLink") && arg.SubLink.subLinkType === "EXISTS_SUBLINK" && arg.SubLink.subselect) {
425
- const subselect = arg.SubLink.subselect;
426
- if (isANode$1(subselect) && is$1(subselect, "SelectStmt") && subselect.SelectStmt.whereClause) {
427
- const where = subselect.SelectStmt.whereClause;
428
- if (isANode$1(where) && is$1(where, "BoolExpr") && where.BoolExpr.boolop === "OR_EXPR") nudges.push({
429
- kind: "FLATTEN_NOT_EXISTS_OR",
430
- severity: "INFO",
431
- message: "Consider splitting NOT EXISTS with OR into separate NOT EXISTS joined by AND",
432
- location: node.BoolExpr.location
433
- });
434
- }
435
- }
436
- }
437
- }
438
142
  if (is$1(node, "A_Expr")) {
439
143
  if (node.A_Expr.kind === "AEXPR_IN") {
440
144
  let list;
@@ -448,36 +152,6 @@ function parseNudges(node, stack) {
448
152
  });
449
153
  }
450
154
  }
451
- if (is$1(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
452
- kind: "PREFER_UNION_ALL_OVER_UNION",
453
- severity: "INFO",
454
- message: "UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed"
455
- });
456
- if (is$1(node, "SelectStmt")) {
457
- const subqueryAliases = /* @__PURE__ */ new Set();
458
- if (node.SelectStmt.targetList) {
459
- for (const target of node.SelectStmt.targetList) if (is$1(target, "ResTarget")) {
460
- if (target.ResTarget.val && is$1(target.ResTarget.val, "SubLink")) {
461
- if (target.ResTarget.name) subqueryAliases.add(target.ResTarget.name.toLowerCase());
462
- }
463
- }
464
- }
465
- if (subqueryAliases.size > 0 && node.SelectStmt.sortClause) {
466
- for (const sortBy of node.SelectStmt.sortClause) if (is$1(sortBy, "SortBy") && sortBy.SortBy.node) {
467
- if (is$1(sortBy.SortBy.node, "ColumnRef")) {
468
- const columnName = extractColumnName(sortBy.SortBy.node);
469
- if (columnName && subqueryAliases.has(columnName.toLowerCase())) {
470
- nudges.push({
471
- kind: "AVOID_SORTING_ON_GENERATED_VALUES",
472
- severity: "WARNING",
473
- message: "Be careful sorting on values generated dynamically during query execution - these cannot use indexes and contribute to slower query performance."
474
- });
475
- break;
476
- }
477
- }
478
- }
479
- }
480
- }
481
155
  return nudges;
482
156
  }
483
157
  function containsColumnRef(args) {
@@ -513,142 +187,6 @@ function getStringConstantValue(node) {
513
187
  if (isANode$1(node) && is$1(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
514
188
  return null;
515
189
  }
516
- const AGGREGATE_FUNCTIONS = new Set([
517
- "count",
518
- "sum",
519
- "avg",
520
- "min",
521
- "max",
522
- "array_agg",
523
- "string_agg",
524
- "bool_and",
525
- "bool_or",
526
- "every"
527
- ]);
528
- function containsAggregate(node) {
529
- if (!node || typeof node !== "object") return false;
530
- if (Array.isArray(node)) return node.some(containsAggregate);
531
- if (isANode$1(node) && is$1(node, "FuncCall")) {
532
- const funcname = node.FuncCall.funcname;
533
- if (funcname) {
534
- for (const f of funcname) if (isANode$1(f) && is$1(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
535
- }
536
- }
537
- if (isANode$1(node)) return containsAggregate(node[Object.keys(node)[0]]);
538
- for (const child of Object.values(node)) if (containsAggregate(child)) return true;
539
- return false;
540
- }
541
- function getLastColumnRefField(columnRef) {
542
- const fields = columnRef.ColumnRef.fields;
543
- if (!fields || fields.length === 0) return null;
544
- const lastField = fields[fields.length - 1];
545
- if (isANode$1(lastField) && is$1(lastField, "String")) return lastField.String.sval || null;
546
- return null;
547
- }
548
- function whereHasIsNotNull(whereClause, columnName) {
549
- if (!whereClause) return false;
550
- let found = false;
551
- Walker.shallowMatch(whereClause, "NullTest", (node) => {
552
- if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is$1(node.NullTest.arg, "ColumnRef")) {
553
- if (getLastColumnRefField(node.NullTest.arg) === columnName) found = true;
554
- }
555
- });
556
- return found;
557
- }
558
- function isCountFuncCall(node) {
559
- if (!node || typeof node !== "object") return false;
560
- if (!isANode$1(node) || !is$1(node, "FuncCall")) return false;
561
- const fc = node.FuncCall;
562
- if (!(fc.funcname?.some((n) => is$1(n, "String") && n.String.sval === "count") ?? false)) return false;
563
- if (fc.agg_star) return true;
564
- if (fc.args && fc.args.length === 1 && isANode$1(fc.args[0]) && is$1(fc.args[0], "A_Const")) return true;
565
- return false;
566
- }
567
- function isSubLinkWithCount(node) {
568
- if (!node || typeof node !== "object") return false;
569
- if (!isANode$1(node) || !is$1(node, "SubLink")) return false;
570
- const subselect = node.SubLink.subselect;
571
- if (!subselect || !isANode$1(subselect) || !is$1(subselect, "SelectStmt")) return false;
572
- const targets = subselect.SelectStmt.targetList;
573
- if (!targets || targets.length !== 1) return false;
574
- const target = targets[0];
575
- if (!isANode$1(target) || !is$1(target, "ResTarget") || !target.ResTarget.val) return false;
576
- return isCountFuncCall(target.ResTarget.val);
577
- }
578
- function isCountExpression(node) {
579
- return isCountFuncCall(node) || isSubLinkWithCount(node);
580
- }
581
- function getIntegerConstantValue(node) {
582
- if (!node || typeof node !== "object") return null;
583
- if (!isANode$1(node) || !is$1(node, "A_Const")) return null;
584
- if (node.A_Const.ival === void 0) return null;
585
- return node.A_Const.ival.ival ?? 0;
586
- }
587
- function isExistenceCheckPattern(lexpr, rexpr, op) {
588
- if (isCountExpression(lexpr)) {
589
- const val = getIntegerConstantValue(rexpr);
590
- if (val !== null) {
591
- if (op === ">" && val === 0) return true;
592
- if (op === ">=" && val === 1) return true;
593
- if ((op === "!=" || op === "<>") && val === 0) return true;
594
- }
595
- }
596
- if (isCountExpression(rexpr)) {
597
- const val = getIntegerConstantValue(lexpr);
598
- if (val !== null) {
599
- if (op === "<" && val === 0) return true;
600
- if (op === "<=" && val === 1) return true;
601
- if ((op === "!=" || op === "<>") && val === 0) return true;
602
- }
603
- }
604
- return false;
605
- }
606
- function collectEqualityConstrainedColumns(whereClause) {
607
- const columns = /* @__PURE__ */ new Set();
608
- function walk(node) {
609
- if (!node || typeof node !== "object") return;
610
- if (!isANode$1(node)) return;
611
- if (is$1(node, "BoolExpr") && node.BoolExpr.boolop === "AND_EXPR" && node.BoolExpr.args) {
612
- for (const arg of node.BoolExpr.args) walk(arg);
613
- return;
614
- }
615
- if (is$1(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
616
- const opName = node.A_Expr.name?.[0];
617
- if (opName && is$1(opName, "String") && opName.String.sval === "=") {
618
- addColumnIfConstant(node.A_Expr.lexpr, node.A_Expr.rexpr, columns);
619
- addColumnIfConstant(node.A_Expr.rexpr, node.A_Expr.lexpr, columns);
620
- }
621
- return;
622
- }
623
- if (is$1(node, "A_Expr") && node.A_Expr.kind === "AEXPR_IN") {
624
- const listNode = node.A_Expr.rexpr;
625
- if (listNode && isANode$1(listNode) && is$1(listNode, "List") && listNode.List.items && listNode.List.items.length === 1) {
626
- const colNode = node.A_Expr.lexpr;
627
- if (colNode && isANode$1(colNode) && is$1(colNode, "ColumnRef")) {
628
- const lastField = colNode.ColumnRef.fields?.[colNode.ColumnRef.fields.length - 1];
629
- if (lastField && is$1(lastField, "String") && lastField.String.sval) columns.add(lastField.String.sval);
630
- }
631
- }
632
- }
633
- }
634
- walk(whereClause);
635
- return columns;
636
- }
637
- function addColumnIfConstant(colSide, constSide, columns) {
638
- if (!colSide || !constSide || typeof colSide !== "object" || typeof constSide !== "object") return;
639
- if (!isANode$1(colSide) || !is$1(colSide, "ColumnRef")) return;
640
- if (!isANode$1(constSide) || !is$1(constSide, "A_Const") && !is$1(constSide, "ParamRef")) return;
641
- const fields = colSide.ColumnRef.fields;
642
- if (!fields || fields.length === 0) return;
643
- const lastField = fields[fields.length - 1];
644
- if (is$1(lastField, "String") && lastField.String.sval) columns.add(lastField.String.sval);
645
- }
646
- function extractColumnName(node) {
647
- if (!node.ColumnRef.fields || node.ColumnRef.fields.length === 0) return null;
648
- const lastField = node.ColumnRef.fields[node.ColumnRef.fields.length - 1];
649
- if (is$1(lastField, "String") && lastField.String.sval) return lastField.String.sval;
650
- return null;
651
- }
652
190
  function countBoolOrConditions(node) {
653
191
  if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
654
192
  let count = 0;
@@ -656,28 +194,6 @@ function countBoolOrConditions(node) {
656
194
  else count += 1;
657
195
  return count;
658
196
  }
659
- /**
660
- * Extract the major version number from a PostgreSQL version string.
661
- * PostgreSQL encodes versions as XXYYZZ where XX is major, YY is minor, ZZ is patch.
662
- * e.g. "170000" → 17, "160004" → 16, "120000" → 12
663
- */
664
- function parseMajorVersion(versionNum) {
665
- return Math.floor(parseInt(versionNum, 10) / 1e4);
666
- }
667
- /**
668
- * Produce version-aware nudges by checking existing nudges against the
669
- * user's PostgreSQL major version. Returns upgrade recommendations
670
- * when a query pattern would benefit from a newer version.
671
- */
672
- function parseVersionNudges(nudges, majorVersion) {
673
- const versionNudges = [];
674
- if (majorVersion < 17 && nudges.some((n) => n.kind === "AVOID_DISTINCT_WITHOUT_REASON")) versionNudges.push({
675
- kind: "UPGRADE_DISTINCT_PG17",
676
- severity: "INFO",
677
- message: "PostgreSQL 17 introduces hash-based DISTINCT, which can significantly speed up queries using DISTINCT"
678
- });
679
- return versionNudges;
680
- }
681
197
 
682
198
  //#endregion
683
199
  //#region \0@oxc-project+runtime@0.112.0/helpers/typeof.js
@@ -808,11 +324,6 @@ var Walker = class Walker {
808
324
  if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "ColumnRef")) this.add(node.A_Expr.lexpr, { jsonbOperator });
809
325
  if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "ColumnRef")) this.add(node.A_Expr.rexpr, { jsonbOperator });
810
326
  }
811
- if (opName && (opName === "&&" || opName === "-|-")) {
812
- const gistOperator = opName;
813
- if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "ColumnRef")) this.add(node.A_Expr.lexpr, { gistOperator });
814
- if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "ColumnRef")) this.add(node.A_Expr.rexpr, { gistOperator });
815
- }
816
327
  }
817
328
  if (is(node, "ColumnRef")) {
818
329
  for (let i = 0; i < stack.length; i++) {
@@ -896,7 +407,6 @@ var Walker = class Walker {
896
407
  if (options?.sort) ref.sort = options.sort;
897
408
  if (options?.where) ref.where = options.where;
898
409
  if (options?.jsonbOperator) ref.jsonbOperator = options.jsonbOperator;
899
- if (options?.gistOperator) ref.gistOperator = options.gistOperator;
900
410
  this.highlights.push(ref);
901
411
  }
902
412
  /**
@@ -1065,7 +575,6 @@ var Analyzer = class {
1065
575
  if (colReference.sort) index.sort = colReference.sort;
1066
576
  if (colReference.where) index.where = colReference.where;
1067
577
  if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
1068
- if (colReference.gistOperator) index.gistOperator = colReference.gistOperator;
1069
578
  addIndex(index);
1070
579
  }
1071
580
  } else if (tableReference) {
@@ -1082,7 +591,6 @@ var Analyzer = class {
1082
591
  if (colReference.sort) index.sort = colReference.sort;
1083
592
  if (colReference.where) index.where = colReference.where;
1084
593
  if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
1085
- if (colReference.gistOperator) index.gistOperator = colReference.gistOperator;
1086
594
  addIndex(index);
1087
595
  }
1088
596
  } else if (fullReference) {
@@ -1095,7 +603,6 @@ var Analyzer = class {
1095
603
  if (colReference.sort) index.sort = colReference.sort;
1096
604
  if (colReference.where) index.where = colReference.where;
1097
605
  if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
1098
- if (colReference.gistOperator) index.gistOperator = colReference.gistOperator;
1099
606
  addIndex(index);
1100
607
  } else {
1101
608
  console.error("Column reference has too many parts. The query is malformed", colReference);
@@ -1207,7 +714,7 @@ async function dropIndex(tx, index) {
1207
714
  //#endregion
1208
715
  //#region src/sql/indexes.ts
1209
716
  function isIndexSupported(index) {
1210
- return index.index_type === "btree" || index.index_type === "gin" || index.index_type === "gist";
717
+ return index.index_type === "btree" || index.index_type === "gin";
1211
718
  }
1212
719
  /**
1213
720
  * Doesn't necessarily decide whether the index can be dropped but can be
@@ -1641,9 +1148,8 @@ var IndexOptimizer = class IndexOptimizer {
1641
1148
  * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
1642
1149
  **/
1643
1150
  indexesToCreate(rootCandidates) {
1644
- const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator && !c.gistOperator);
1151
+ const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
1645
1152
  const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
1646
- const gistCandidates = rootCandidates.filter((c) => c.gistOperator);
1647
1153
  const nextStage = [];
1648
1154
  const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
1649
1155
  for (const permutation of permutedIndexes.values()) {
@@ -1697,32 +1203,6 @@ var IndexOptimizer = class IndexOptimizer {
1697
1203
  opclass
1698
1204
  });
1699
1205
  }
1700
- const gistGroups = this.groupGistCandidatesByColumn(gistCandidates);
1701
- for (const group of gistGroups.values()) {
1702
- const { schema: rawSchema, table: rawTable, column } = group;
1703
- const schema = PgIdentifier.fromString(rawSchema);
1704
- const table = PgIdentifier.fromString(rawTable);
1705
- if (this.gistIndexAlreadyExists(table.toString(), column)) continue;
1706
- const indexName = this.indexName();
1707
- const candidate = {
1708
- schema: rawSchema,
1709
- table: rawTable,
1710
- column
1711
- };
1712
- const definition = this.toGistDefinition({
1713
- table,
1714
- schema,
1715
- column: PgIdentifier.fromString(column)
1716
- });
1717
- nextStage.push({
1718
- name: indexName,
1719
- schema: schema.toString(),
1720
- table: table.toString(),
1721
- columns: [candidate],
1722
- definition,
1723
- indexMethod: "gist"
1724
- });
1725
- }
1726
1206
  return nextStage;
1727
1207
  }
1728
1208
  toDefinition({ schema, table, columns }) {
@@ -1753,12 +1233,6 @@ var IndexOptimizer = class IndexOptimizer {
1753
1233
  const opclassSuffix = opclass ? ` ${opclass}` : "";
1754
1234
  return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
1755
1235
  }
1756
- toGistDefinition({ schema, table, column }) {
1757
- let fullyQualifiedTable;
1758
- if (schema.toString() === "public") fullyQualifiedTable = table;
1759
- else fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
1760
- return `${fullyQualifiedTable} using gist (${column})`;
1761
- }
1762
1236
  groupGinCandidatesByColumn(candidates) {
1763
1237
  const groups = /* @__PURE__ */ new Map();
1764
1238
  for (const c of candidates) {
@@ -1779,22 +1253,6 @@ var IndexOptimizer = class IndexOptimizer {
1779
1253
  ginIndexAlreadyExists(table, column) {
1780
1254
  return this.existingIndexes.find((index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column));
1781
1255
  }
1782
- groupGistCandidatesByColumn(candidates) {
1783
- const groups = /* @__PURE__ */ new Map();
1784
- for (const c of candidates) {
1785
- if (!c.gistOperator) continue;
1786
- const key = `${c.schema}.${c.table}.${c.column}`;
1787
- if (!groups.has(key)) groups.set(key, {
1788
- schema: c.schema,
1789
- table: c.table,
1790
- column: c.column
1791
- });
1792
- }
1793
- return groups;
1794
- }
1795
- gistIndexAlreadyExists(table, column) {
1796
- return this.existingIndexes.find((index) => index.index_type === "gist" && index.table_name === table && index.index_columns.some((c) => c.name === column));
1797
- }
1798
1256
  /**
1799
1257
  * Drop indexes that can be dropped. Ignore the ones that can't
1800
1258
  */
@@ -2569,5 +2027,5 @@ var PssRewriter = class {
2569
2027
  };
2570
2028
 
2571
2029
  //#endregion
2572
- export { Analyzer, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexOptimizer, PROCEED, PgIdentifier, PostgresQueryBuilder, PostgresVersion, PssRewriter, SKIP, Statistics, StatisticsMode, StatisticsSource, dropIndex, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseMajorVersion, parseNudges, parseVersionNudges };
2030
+ export { Analyzer, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexOptimizer, PROCEED, PgIdentifier, PostgresQueryBuilder, PostgresVersion, PssRewriter, SKIP, Statistics, StatisticsMode, StatisticsSource, dropIndex, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
2573
2031
  //# sourceMappingURL=index.mjs.map