@query-doctor/core 0.10.7 → 0.10.9
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 +3 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/sql/normalized-fingerprint.cjs.map +1 -1
- package/dist/sql/normalized-fingerprint.d.cts.map +1 -1
- package/dist/sql/normalized-fingerprint.d.mts.map +1 -1
- package/dist/sql/normalized-fingerprint.mjs.map +1 -1
- package/dist/sql/nudges.cjs +7 -5
- package/dist/sql/nudges.cjs.map +1 -1
- package/dist/sql/nudges.mjs +7 -5
- package/dist/sql/nudges.mjs.map +1 -1
- package/dist/sql/test-origin.cjs +52 -0
- package/dist/sql/test-origin.cjs.map +1 -0
- package/dist/sql/test-origin.d.cts +34 -0
- package/dist/sql/test-origin.d.cts.map +1 -0
- package/dist/sql/test-origin.d.mts +34 -0
- package/dist/sql/test-origin.d.mts.map +1 -0
- package/dist/sql/test-origin.mjs +51 -0
- package/dist/sql/test-origin.mjs.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const require_pg_identifier = require("./sql/pg-identifier.cjs");
|
|
|
4
4
|
const require_schema = require("./schema.cjs");
|
|
5
5
|
const require_nudges = require("./sql/nudges.cjs");
|
|
6
6
|
const require_analyzer = require("./sql/analyzer.cjs");
|
|
7
|
+
const require_test_origin = require("./sql/test-origin.cjs");
|
|
7
8
|
const require_database = require("./sql/database.cjs");
|
|
8
9
|
const require_display_query = require("./sql/display-query.cjs");
|
|
9
10
|
const require_normalized_fingerprint = require("./sql/normalized-fingerprint.cjs");
|
|
@@ -73,5 +74,7 @@ exports.ignoredIdentifier = require_analyzer.ignoredIdentifier;
|
|
|
73
74
|
exports.indexCovers = require_index_coverage.indexCovers;
|
|
74
75
|
exports.isIndexProbablyDroppable = require_indexes.isIndexProbablyDroppable;
|
|
75
76
|
exports.isIndexSupported = require_indexes.isIndexSupported;
|
|
77
|
+
exports.isTestFilePath = require_test_origin.isTestFilePath;
|
|
78
|
+
exports.isTestOriginQuery = require_test_origin.isTestOriginQuery;
|
|
76
79
|
exports.normalizedFingerprint = require_normalized_fingerprint.normalizedFingerprint;
|
|
77
80
|
exports.parseNudges = require_nudges.parseNudges;
|
package/dist/index.d.cts
CHANGED
|
@@ -8,6 +8,7 @@ import { ColumnMetadata, ComputedColumnStats, ComputedReltuples, ComputedStats,
|
|
|
8
8
|
import { IndexIdentifier, IndexOptimizer, IndexToCreate, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP } from "./optimizer/genalgo.cjs";
|
|
9
9
|
import { Nudge, parseNudges } from "./sql/nudges.cjs";
|
|
10
10
|
import { AnalysisResult, Analyzer, DatabaseDriver, DiscoveredColumnReference, JsonbOperator, Parser, SQLCommenterExtraction, SQLCommenterTag, SortContext, StatementType, TableReference, ignoredIdentifier } from "./sql/analyzer.cjs";
|
|
11
|
+
import { isTestFilePath, isTestOriginQuery } from "./sql/test-origin.cjs";
|
|
11
12
|
import { CompactSelectListOptions, compactSelectList } from "./sql/display-query.cjs";
|
|
12
13
|
import { FingerprintFn, normalizedFingerprint } from "./sql/normalized-fingerprint.cjs";
|
|
13
14
|
import { DuplicateIndexGroup, groupDuplicateIndexes, isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.cjs";
|
|
@@ -20,4 +21,4 @@ import { ActionPlanNudgeQuery, ActionableStep, AffectedQueryCost, CostReduction,
|
|
|
20
21
|
import { deriveSentryEnvironment } from "./sentry.cjs";
|
|
21
22
|
import { ClientApi, ConnectionMode, ExtensionPresence, IndexDefinition, RepoConfig, ServerApi, UnauthenticatedServerApi } from "./websocket-server.cjs";
|
|
22
23
|
import { QueryFinding, QueryFindingCode, QueryFindingSeverity, analyzeQueryFindings } from "./findings/query-findings.cjs";
|
|
23
|
-
export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FingerprintFn, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryFinding, QueryFindingCode, QueryFindingSeverity, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, normalizedFingerprint, parseNudges };
|
|
24
|
+
export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FingerprintFn, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryFinding, QueryFindingCode, QueryFindingSeverity, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, isTestFilePath, isTestOriginQuery, normalizedFingerprint, parseNudges };
|
package/dist/index.d.mts
CHANGED
|
@@ -8,6 +8,7 @@ import { ColumnMetadata, ComputedColumnStats, ComputedReltuples, ComputedStats,
|
|
|
8
8
|
import { IndexIdentifier, IndexOptimizer, IndexToCreate, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP } from "./optimizer/genalgo.mjs";
|
|
9
9
|
import { Nudge, parseNudges } from "./sql/nudges.mjs";
|
|
10
10
|
import { AnalysisResult, Analyzer, DatabaseDriver, DiscoveredColumnReference, JsonbOperator, Parser, SQLCommenterExtraction, SQLCommenterTag, SortContext, StatementType, TableReference, ignoredIdentifier } from "./sql/analyzer.mjs";
|
|
11
|
+
import { isTestFilePath, isTestOriginQuery } from "./sql/test-origin.mjs";
|
|
11
12
|
import { CompactSelectListOptions, compactSelectList } from "./sql/display-query.mjs";
|
|
12
13
|
import { FingerprintFn, normalizedFingerprint } from "./sql/normalized-fingerprint.mjs";
|
|
13
14
|
import { DuplicateIndexGroup, groupDuplicateIndexes, isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.mjs";
|
|
@@ -20,4 +21,4 @@ import { ActionPlanNudgeQuery, ActionableStep, AffectedQueryCost, CostReduction,
|
|
|
20
21
|
import { deriveSentryEnvironment } from "./sentry.mjs";
|
|
21
22
|
import { ClientApi, ConnectionMode, ExtensionPresence, IndexDefinition, RepoConfig, ServerApi, UnauthenticatedServerApi } from "./websocket-server.mjs";
|
|
22
23
|
import { QueryFinding, QueryFindingCode, QueryFindingSeverity, analyzeQueryFindings } from "./findings/query-findings.mjs";
|
|
23
|
-
export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FingerprintFn, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryFinding, QueryFindingCode, QueryFindingSeverity, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, normalizedFingerprint, parseNudges };
|
|
24
|
+
export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FingerprintFn, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryFinding, QueryFindingCode, QueryFindingSeverity, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, isTestFilePath, isTestOriginQuery, normalizedFingerprint, parseNudges };
|
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { PgIdentifier } from "./sql/pg-identifier.mjs";
|
|
|
3
3
|
import { FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, dumpSchema } from "./schema.mjs";
|
|
4
4
|
import { parseNudges } from "./sql/nudges.mjs";
|
|
5
5
|
import { Analyzer, ignoredIdentifier } from "./sql/analyzer.mjs";
|
|
6
|
+
import { isTestFilePath, isTestOriginQuery } from "./sql/test-origin.mjs";
|
|
6
7
|
import { PostgresExplainStageSchema, PostgresVersion, dropIndex } from "./sql/database.mjs";
|
|
7
8
|
import { compactSelectList } from "./sql/display-query.mjs";
|
|
8
9
|
import { normalizedFingerprint } from "./sql/normalized-fingerprint.mjs";
|
|
@@ -18,4 +19,4 @@ import { buildActionPlan } from "./action-plan/build-action-plan.mjs";
|
|
|
18
19
|
import { deriveSentryEnvironment } from "./sentry.mjs";
|
|
19
20
|
import { OptimizedQuery, RecentQuery } from "./query.mjs";
|
|
20
21
|
import { analyzeQueryFindings } from "./findings/query-findings.mjs";
|
|
21
|
-
export { Analyzer, CombinedExport, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexOptimizer, OptimizedQuery, PROCEED, PgIdentifier, PostgresExplainStageSchema, PostgresQueryBuilder, PostgresVersion, PssRewriter, RecentQuery, SKIP, Statistics, StatisticsMode, StatisticsSource, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, normalizedFingerprint, parseNudges };
|
|
22
|
+
export { Analyzer, CombinedExport, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexOptimizer, OptimizedQuery, PROCEED, PgIdentifier, PostgresExplainStageSchema, PostgresQueryBuilder, PostgresVersion, PssRewriter, RecentQuery, SKIP, Statistics, StatisticsMode, StatisticsSource, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, isTestFilePath, isTestOriginQuery, normalizedFingerprint, parseNudges };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalized-fingerprint.cjs","names":["is"],"sources":["../../src/sql/normalized-fingerprint.ts"],"sourcesContent":["import type { Node, ParseResult } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport { is } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\n/**\n * A function that fingerprints a SQL string. Injected so this module stays\n * free of a fingerprint runtime dependency, mirroring how `Analyzer` accepts\n * an injected parser. In practice this is `fingerprint` from\n * `@libpg-query/parser`.\n */\nexport type FingerprintFn = (query: string) => Promise<string>;\n\ntype SelectStmt = Extract<Node, { SelectStmt: unknown }>[\"SelectStmt\"];\n\nconst SELECT_LIST_PLACEHOLDER: Node = {\n ResTarget: {\n val: { A_Const: { ival: { ival: 1 }, location: -1 } },\n location: -1,\n },\n};\n\n/**\n * A \"bare column\" target: any `ColumnRef`, including `a`, qualified `t.a`, and\n * the wildcards `*` and `t.*`. `count(*)`, `a + b`, literals, and `AS` aliases\n * are not column refs and survive normalization.\n */\nfunction isBareColumn(target: Node): boolean {\n if (!is(target, \"ResTarget\")) return false;\n const val = target.ResTarget.val;\n if (!val) return false;\n return is(val, \"ColumnRef\");\n}\n\n/**\n * Set-operation arms (`larg`/`rarg`) are inlined `SelectStmt` bodies rather\n * than wrapped `{ SelectStmt }` nodes, so `Walker.traverse` never emits them.\n * Drill into them explicitly; they can nest (`a UNION b UNION c`).\n */\nfunction normalizeSelect(select: SelectStmt): void {\n const targetList = select.targetList;\n if (targetList) {\n const survivors = targetList.filter((t) => !isBareColumn(t));\n if (survivors.length > 1) {\n const keyed = survivors.map((t) => ({ key: deparseSync(t), t }));\n keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));\n select.targetList = keyed.map((k) => k.t);\n } else {\n select.targetList = survivors.length
|
|
1
|
+
{"version":3,"file":"normalized-fingerprint.cjs","names":["is"],"sources":["../../src/sql/normalized-fingerprint.ts"],"sourcesContent":["import type { Node, ParseResult } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport { is } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\n/**\n * A function that fingerprints a SQL string. Injected so this module stays\n * free of a fingerprint runtime dependency, mirroring how `Analyzer` accepts\n * an injected parser. In practice this is `fingerprint` from\n * `@libpg-query/parser`.\n */\nexport type FingerprintFn = (query: string) => Promise<string>;\n\ntype SelectStmt = Extract<Node, { SelectStmt: unknown }>[\"SelectStmt\"];\n\nconst SELECT_LIST_PLACEHOLDER: Node = {\n ResTarget: {\n val: { A_Const: { ival: { ival: 1 }, location: -1 } },\n location: -1,\n },\n};\n\n/**\n * A \"bare column\" target: any `ColumnRef`, including `a`, qualified `t.a`, and\n * the wildcards `*` and `t.*`. `count(*)`, `a + b`, literals, and `AS` aliases\n * are not column refs and survive normalization.\n */\nfunction isBareColumn(target: Node): boolean {\n if (!is(target, \"ResTarget\")) return false;\n const val = target.ResTarget.val;\n if (!val) return false;\n return is(val, \"ColumnRef\");\n}\n\n/**\n * Set-operation arms (`larg`/`rarg`) are inlined `SelectStmt` bodies rather\n * than wrapped `{ SelectStmt }` nodes, so `Walker.traverse` never emits them.\n * Drill into them explicitly; they can nest (`a UNION b UNION c`).\n */\nfunction normalizeSelect(select: SelectStmt): void {\n const targetList = select.targetList;\n if (targetList) {\n const survivors = targetList.filter((t) => !isBareColumn(t));\n if (survivors.length > 1) {\n const keyed = survivors.map((t) => ({ key: deparseSync(t), t }));\n keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));\n select.targetList = keyed.map((k) => k.t);\n } else {\n select.targetList = survivors.length\n ? survivors\n : [SELECT_LIST_PLACEHOLDER];\n }\n }\n if (select.larg) normalizeSelect(select.larg);\n if (select.rarg) normalizeSelect(select.rarg);\n}\n\n/**\n * Fingerprint a query while normalizing every SELECT target list in the tree\n * so that the presence and order of bare column references don't affect the\n * result. Applies to the top-level SELECT as well as SELECTs nested in CTEs,\n * subqueries, and set-operation arms. A change to the set of\n * aggregates/expressions, the WHERE, the FROM, etc. still moves it.\n *\n * Takes the already-parsed AST and an injected {@link FingerprintFn} so core\n * needs no parser or fingerprint runtime. The AST is cloned before mutation.\n */\nexport async function normalizedFingerprint(\n ast: ParseResult,\n fingerprint: FingerprintFn,\n): Promise<string> {\n const cloned: ParseResult = JSON.parse(JSON.stringify(ast));\n\n for (const raw of cloned.stmts ?? []) {\n if (!raw.stmt) continue;\n Walker.traverse(raw.stmt, (node) => {\n if (is(node, \"SelectStmt\")) {\n normalizeSelect(node.SelectStmt);\n }\n });\n }\n\n return fingerprint(deparseSync(cloned));\n}\n"],"mappings":";;;;;;AAeA,MAAM,0BAAgC,EACpC,WAAW;CACT,KAAK,EAAE,SAAS;EAAE,MAAM,EAAE,MAAM,GAAG;EAAE,UAAU;EAAI,EAAE;CACrD,UAAU;CACX,EACF;;;;;;AAOD,SAAS,aAAa,QAAuB;AAC3C,KAAI,CAACA,kBAAAA,GAAG,QAAQ,YAAY,CAAE,QAAO;CACrC,MAAM,MAAM,OAAO,UAAU;AAC7B,KAAI,CAAC,IAAK,QAAO;AACjB,QAAOA,kBAAAA,GAAG,KAAK,YAAY;;;;;;;AAQ7B,SAAS,gBAAgB,QAA0B;CACjD,MAAM,aAAa,OAAO;AAC1B,KAAI,YAAY;EACd,MAAM,YAAY,WAAW,QAAQ,MAAM,CAAC,aAAa,EAAE,CAAC;AAC5D,MAAI,UAAU,SAAS,GAAG;GACxB,MAAM,QAAQ,UAAU,KAAK,OAAO;IAAE,MAAA,GAAA,eAAA,aAAiB,EAAE;IAAE;IAAG,EAAE;AAChE,SAAM,MAAM,GAAG,MAAO,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,EAAG;AAClE,UAAO,aAAa,MAAM,KAAK,MAAM,EAAE,EAAE;QAEzC,QAAO,aAAa,UAAU,SAC1B,YACA,CAAC,wBAAwB;;AAGjC,KAAI,OAAO,KAAM,iBAAgB,OAAO,KAAK;AAC7C,KAAI,OAAO,KAAM,iBAAgB,OAAO,KAAK;;;;;;;;;;;;AAa/C,eAAsB,sBACpB,KACA,aACiB;CACjB,MAAM,SAAsB,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC;AAE3D,MAAK,MAAM,OAAO,OAAO,SAAS,EAAE,EAAE;AACpC,MAAI,CAAC,IAAI,KAAM;AACf,iBAAA,OAAO,SAAS,IAAI,OAAO,SAAS;AAClC,OAAIA,kBAAAA,GAAG,MAAM,aAAa,CACxB,iBAAgB,KAAK,WAAW;IAElC;;AAGJ,QAAO,aAAA,GAAA,eAAA,aAAwB,OAAO,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalized-fingerprint.d.cts","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"mappings":";;;;;;;;AAWA;;;KAAY,aAAA,IAAiB,KAAA,aAAkB,OAAA;;
|
|
1
|
+
{"version":3,"file":"normalized-fingerprint.d.cts","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"mappings":";;;;;;;;AAWA;;;KAAY,aAAA,IAAiB,KAAA,aAAkB,OAAA;;AAwD/C;;;;;;;;;iBAAsB,qBAAA,CACpB,GAAA,EAAK,WAAA,EACL,WAAA,EAAa,aAAA,GACZ,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalized-fingerprint.d.mts","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"mappings":";;;;;;;;AAWA;;;KAAY,aAAA,IAAiB,KAAA,aAAkB,OAAA;;
|
|
1
|
+
{"version":3,"file":"normalized-fingerprint.d.mts","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"mappings":";;;;;;;;AAWA;;;KAAY,aAAA,IAAiB,KAAA,aAAkB,OAAA;;AAwD/C;;;;;;;;;iBAAsB,qBAAA,CACpB,GAAA,EAAK,WAAA,EACL,WAAA,EAAa,aAAA,GACZ,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalized-fingerprint.mjs","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"sourcesContent":["import type { Node, ParseResult } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport { is } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\n/**\n * A function that fingerprints a SQL string. Injected so this module stays\n * free of a fingerprint runtime dependency, mirroring how `Analyzer` accepts\n * an injected parser. In practice this is `fingerprint` from\n * `@libpg-query/parser`.\n */\nexport type FingerprintFn = (query: string) => Promise<string>;\n\ntype SelectStmt = Extract<Node, { SelectStmt: unknown }>[\"SelectStmt\"];\n\nconst SELECT_LIST_PLACEHOLDER: Node = {\n ResTarget: {\n val: { A_Const: { ival: { ival: 1 }, location: -1 } },\n location: -1,\n },\n};\n\n/**\n * A \"bare column\" target: any `ColumnRef`, including `a`, qualified `t.a`, and\n * the wildcards `*` and `t.*`. `count(*)`, `a + b`, literals, and `AS` aliases\n * are not column refs and survive normalization.\n */\nfunction isBareColumn(target: Node): boolean {\n if (!is(target, \"ResTarget\")) return false;\n const val = target.ResTarget.val;\n if (!val) return false;\n return is(val, \"ColumnRef\");\n}\n\n/**\n * Set-operation arms (`larg`/`rarg`) are inlined `SelectStmt` bodies rather\n * than wrapped `{ SelectStmt }` nodes, so `Walker.traverse` never emits them.\n * Drill into them explicitly; they can nest (`a UNION b UNION c`).\n */\nfunction normalizeSelect(select: SelectStmt): void {\n const targetList = select.targetList;\n if (targetList) {\n const survivors = targetList.filter((t) => !isBareColumn(t));\n if (survivors.length > 1) {\n const keyed = survivors.map((t) => ({ key: deparseSync(t), t }));\n keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));\n select.targetList = keyed.map((k) => k.t);\n } else {\n select.targetList = survivors.length
|
|
1
|
+
{"version":3,"file":"normalized-fingerprint.mjs","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"sourcesContent":["import type { Node, ParseResult } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport { is } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\n/**\n * A function that fingerprints a SQL string. Injected so this module stays\n * free of a fingerprint runtime dependency, mirroring how `Analyzer` accepts\n * an injected parser. In practice this is `fingerprint` from\n * `@libpg-query/parser`.\n */\nexport type FingerprintFn = (query: string) => Promise<string>;\n\ntype SelectStmt = Extract<Node, { SelectStmt: unknown }>[\"SelectStmt\"];\n\nconst SELECT_LIST_PLACEHOLDER: Node = {\n ResTarget: {\n val: { A_Const: { ival: { ival: 1 }, location: -1 } },\n location: -1,\n },\n};\n\n/**\n * A \"bare column\" target: any `ColumnRef`, including `a`, qualified `t.a`, and\n * the wildcards `*` and `t.*`. `count(*)`, `a + b`, literals, and `AS` aliases\n * are not column refs and survive normalization.\n */\nfunction isBareColumn(target: Node): boolean {\n if (!is(target, \"ResTarget\")) return false;\n const val = target.ResTarget.val;\n if (!val) return false;\n return is(val, \"ColumnRef\");\n}\n\n/**\n * Set-operation arms (`larg`/`rarg`) are inlined `SelectStmt` bodies rather\n * than wrapped `{ SelectStmt }` nodes, so `Walker.traverse` never emits them.\n * Drill into them explicitly; they can nest (`a UNION b UNION c`).\n */\nfunction normalizeSelect(select: SelectStmt): void {\n const targetList = select.targetList;\n if (targetList) {\n const survivors = targetList.filter((t) => !isBareColumn(t));\n if (survivors.length > 1) {\n const keyed = survivors.map((t) => ({ key: deparseSync(t), t }));\n keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));\n select.targetList = keyed.map((k) => k.t);\n } else {\n select.targetList = survivors.length\n ? survivors\n : [SELECT_LIST_PLACEHOLDER];\n }\n }\n if (select.larg) normalizeSelect(select.larg);\n if (select.rarg) normalizeSelect(select.rarg);\n}\n\n/**\n * Fingerprint a query while normalizing every SELECT target list in the tree\n * so that the presence and order of bare column references don't affect the\n * result. Applies to the top-level SELECT as well as SELECTs nested in CTEs,\n * subqueries, and set-operation arms. A change to the set of\n * aggregates/expressions, the WHERE, the FROM, etc. still moves it.\n *\n * Takes the already-parsed AST and an injected {@link FingerprintFn} so core\n * needs no parser or fingerprint runtime. The AST is cloned before mutation.\n */\nexport async function normalizedFingerprint(\n ast: ParseResult,\n fingerprint: FingerprintFn,\n): Promise<string> {\n const cloned: ParseResult = JSON.parse(JSON.stringify(ast));\n\n for (const raw of cloned.stmts ?? []) {\n if (!raw.stmt) continue;\n Walker.traverse(raw.stmt, (node) => {\n if (is(node, \"SelectStmt\")) {\n normalizeSelect(node.SelectStmt);\n }\n });\n }\n\n return fingerprint(deparseSync(cloned));\n}\n"],"mappings":";;;;;AAeA,MAAM,0BAAgC,EACpC,WAAW;CACT,KAAK,EAAE,SAAS;EAAE,MAAM,EAAE,MAAM,GAAG;EAAE,UAAU;EAAI,EAAE;CACrD,UAAU;CACX,EACF;;;;;;AAOD,SAAS,aAAa,QAAuB;AAC3C,KAAI,CAAC,GAAG,QAAQ,YAAY,CAAE,QAAO;CACrC,MAAM,MAAM,OAAO,UAAU;AAC7B,KAAI,CAAC,IAAK,QAAO;AACjB,QAAO,GAAG,KAAK,YAAY;;;;;;;AAQ7B,SAAS,gBAAgB,QAA0B;CACjD,MAAM,aAAa,OAAO;AAC1B,KAAI,YAAY;EACd,MAAM,YAAY,WAAW,QAAQ,MAAM,CAAC,aAAa,EAAE,CAAC;AAC5D,MAAI,UAAU,SAAS,GAAG;GACxB,MAAM,QAAQ,UAAU,KAAK,OAAO;IAAE,KAAK,YAAY,EAAE;IAAE;IAAG,EAAE;AAChE,SAAM,MAAM,GAAG,MAAO,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,EAAG;AAClE,UAAO,aAAa,MAAM,KAAK,MAAM,EAAE,EAAE;QAEzC,QAAO,aAAa,UAAU,SAC1B,YACA,CAAC,wBAAwB;;AAGjC,KAAI,OAAO,KAAM,iBAAgB,OAAO,KAAK;AAC7C,KAAI,OAAO,KAAM,iBAAgB,OAAO,KAAK;;;;;;;;;;;;AAa/C,eAAsB,sBACpB,KACA,aACiB;CACjB,MAAM,SAAsB,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC;AAE3D,MAAK,MAAM,OAAO,OAAO,SAAS,EAAE,EAAE;AACpC,MAAI,CAAC,IAAI,KAAM;AACf,SAAO,SAAS,IAAI,OAAO,SAAS;AAClC,OAAI,GAAG,MAAM,aAAa,CACxB,iBAAgB,KAAK,WAAW;IAElC;;AAGJ,QAAO,YAAY,YAAY,OAAO,CAAC"}
|
package/dist/sql/nudges.cjs
CHANGED
|
@@ -74,7 +74,8 @@ function parseNudges(node, stack) {
|
|
|
74
74
|
message: "Missing WHERE clause",
|
|
75
75
|
location: fromLocation
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
const isBareAggregate = !node.SelectStmt.groupClause?.length && containsAggregate(node.SelectStmt.targetList, true);
|
|
78
|
+
if (!node.SelectStmt.limitCount && !isBareAggregate) nudges.push({
|
|
78
79
|
kind: "MISSING_LIMIT_CLAUSE",
|
|
79
80
|
severity: "INFO",
|
|
80
81
|
message: "Missing LIMIT clause",
|
|
@@ -350,17 +351,18 @@ const AGGREGATE_FUNCTIONS = new Set([
|
|
|
350
351
|
"bool_or",
|
|
351
352
|
"every"
|
|
352
353
|
]);
|
|
353
|
-
function containsAggregate(node) {
|
|
354
|
+
function containsAggregate(node, skipSubqueries = false) {
|
|
354
355
|
if (!node || typeof node !== "object") return false;
|
|
355
|
-
if (Array.isArray(node)) return node.some(containsAggregate);
|
|
356
|
+
if (Array.isArray(node)) return node.some((child) => containsAggregate(child, skipSubqueries));
|
|
357
|
+
if (skipSubqueries && require_ast_utils.isANode(node) && require_ast_utils.is(node, "SubLink")) return false;
|
|
356
358
|
if (require_ast_utils.isANode(node) && require_ast_utils.is(node, "FuncCall")) {
|
|
357
359
|
const funcname = node.FuncCall.funcname;
|
|
358
360
|
if (funcname) {
|
|
359
361
|
for (const f of funcname) if (require_ast_utils.isANode(f) && require_ast_utils.is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
|
-
if (require_ast_utils.isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
|
|
363
|
-
for (const child of Object.values(node)) if (containsAggregate(child)) return true;
|
|
364
|
+
if (require_ast_utils.isANode(node)) return containsAggregate(node[Object.keys(node)[0]], skipSubqueries);
|
|
365
|
+
for (const child of Object.values(node)) if (containsAggregate(child, skipSubqueries)) return true;
|
|
364
366
|
return false;
|
|
365
367
|
}
|
|
366
368
|
function countBoolOrConditions(node) {
|
package/dist/sql/nudges.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudges.cjs","names":["is","isANode"],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\"\n | \"LIMIT_WITHOUT_ORDER_BY\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect LIMIT without ORDER BY (non-deterministic results)\n // LIMIT ALL parses as limitCount with A_Const.isnull — exclude it\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.limitCount &&\n !isNullConstant(node.SelectStmt.limitCount) &&\n !node.SelectStmt.sortClause\n ) {\n nudges.push({\n kind: \"LIMIT_WITHOUT_ORDER_BY\",\n severity: \"WARNING\",\n message:\n \"LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output\",\n });\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"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.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nudges.cjs","names":["is","isANode"],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\"\n | \"LIMIT_WITHOUT_ORDER_BY\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n // A bare aggregate select (aggregates in the target list, no GROUP BY)\n // returns exactly one row, so a LIMIT adds nothing. Aggregates with\n // GROUP BY can still return many rows and stay eligible.\n const isBareAggregate =\n !node.SelectStmt.groupClause?.length &&\n containsAggregate(node.SelectStmt.targetList, true);\n\n if (!node.SelectStmt.limitCount && !isBareAggregate) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect LIMIT without ORDER BY (non-deterministic results)\n // LIMIT ALL parses as limitCount with A_Const.isnull — exclude it\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.limitCount &&\n !isNullConstant(node.SelectStmt.limitCount) &&\n !node.SelectStmt.sortClause\n ) {\n nudges.push({\n kind: \"LIMIT_WITHOUT_ORDER_BY\",\n severity: \"WARNING\",\n message:\n \"LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output\",\n });\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"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.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown, skipSubqueries = false): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => containsAggregate(child, skipSubqueries));\n }\n\n // A subquery's aggregate doesn't bound the enclosing query, so callers that\n // reason about the enclosing statement (e.g. bare-aggregate detection) skip it.\n if (skipSubqueries && isANode(node) && is(node, \"SubLink\")) {\n return false;\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]], skipSubqueries);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child, skipSubqueries)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
package/dist/sql/nudges.mjs
CHANGED
|
@@ -74,7 +74,8 @@ function parseNudges(node, stack) {
|
|
|
74
74
|
message: "Missing WHERE clause",
|
|
75
75
|
location: fromLocation
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
const isBareAggregate = !node.SelectStmt.groupClause?.length && containsAggregate(node.SelectStmt.targetList, true);
|
|
78
|
+
if (!node.SelectStmt.limitCount && !isBareAggregate) nudges.push({
|
|
78
79
|
kind: "MISSING_LIMIT_CLAUSE",
|
|
79
80
|
severity: "INFO",
|
|
80
81
|
message: "Missing LIMIT clause",
|
|
@@ -350,17 +351,18 @@ const AGGREGATE_FUNCTIONS = new Set([
|
|
|
350
351
|
"bool_or",
|
|
351
352
|
"every"
|
|
352
353
|
]);
|
|
353
|
-
function containsAggregate(node) {
|
|
354
|
+
function containsAggregate(node, skipSubqueries = false) {
|
|
354
355
|
if (!node || typeof node !== "object") return false;
|
|
355
|
-
if (Array.isArray(node)) return node.some(containsAggregate);
|
|
356
|
+
if (Array.isArray(node)) return node.some((child) => containsAggregate(child, skipSubqueries));
|
|
357
|
+
if (skipSubqueries && isANode(node) && is(node, "SubLink")) return false;
|
|
356
358
|
if (isANode(node) && is(node, "FuncCall")) {
|
|
357
359
|
const funcname = node.FuncCall.funcname;
|
|
358
360
|
if (funcname) {
|
|
359
361
|
for (const f of funcname) if (isANode(f) && is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
|
-
if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
|
|
363
|
-
for (const child of Object.values(node)) if (containsAggregate(child)) return true;
|
|
364
|
+
if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]], skipSubqueries);
|
|
365
|
+
for (const child of Object.values(node)) if (containsAggregate(child, skipSubqueries)) return true;
|
|
364
366
|
return false;
|
|
365
367
|
}
|
|
366
368
|
function countBoolOrConditions(node) {
|
package/dist/sql/nudges.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudges.mjs","names":[],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\"\n | \"LIMIT_WITHOUT_ORDER_BY\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect LIMIT without ORDER BY (non-deterministic results)\n // LIMIT ALL parses as limitCount with A_Const.isnull — exclude it\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.limitCount &&\n !isNullConstant(node.SelectStmt.limitCount) &&\n !node.SelectStmt.sortClause\n ) {\n nudges.push({\n kind: \"LIMIT_WITHOUT_ORDER_BY\",\n severity: \"WARNING\",\n message:\n \"LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output\",\n });\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"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.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nudges.mjs","names":[],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\"\n | \"LIMIT_WITHOUT_ORDER_BY\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n // A bare aggregate select (aggregates in the target list, no GROUP BY)\n // returns exactly one row, so a LIMIT adds nothing. Aggregates with\n // GROUP BY can still return many rows and stay eligible.\n const isBareAggregate =\n !node.SelectStmt.groupClause?.length &&\n containsAggregate(node.SelectStmt.targetList, true);\n\n if (!node.SelectStmt.limitCount && !isBareAggregate) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect LIMIT without ORDER BY (non-deterministic results)\n // LIMIT ALL parses as limitCount with A_Const.isnull — exclude it\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.limitCount &&\n !isNullConstant(node.SelectStmt.limitCount) &&\n !node.SelectStmt.sortClause\n ) {\n nudges.push({\n kind: \"LIMIT_WITHOUT_ORDER_BY\",\n severity: \"WARNING\",\n message:\n \"LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output\",\n });\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"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.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown, skipSubqueries = false): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => containsAggregate(child, skipSubqueries));\n }\n\n // A subquery's aggregate doesn't bound the enclosing query, so callers that\n // reason about the enclosing statement (e.g. bare-aggregate detection) skip it.\n if (skipSubqueries && isANode(node) && is(node, \"SubLink\")) {\n return false;\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]], skipSubqueries);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child, skipSubqueries)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/sql/test-origin.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether a query originated from a test file, decided from its SQLCommenter
|
|
5
|
+
* `file` tag.
|
|
6
|
+
*
|
|
7
|
+
* The CI gate should never flag a query that no production route runs — e.g. a
|
|
8
|
+
* read-back assertion in `tests/pg/postgres.test.ts` — yet before this such
|
|
9
|
+
* queries had to be ignored one hash at a time (#3199). SQLCommenter already
|
|
10
|
+
* carries the caller's file (surfaced in query detail per #2585); this reads it
|
|
11
|
+
* so both the analyzer's gate and the server's alert engine can bucket
|
|
12
|
+
* test-origin queries out with a single shared rule instead of drifting apart.
|
|
13
|
+
*
|
|
14
|
+
* Conservative by construction: the decision keys only off the `file` tag's
|
|
15
|
+
* origin path. A query with no `file` tag is never test-origin, so untagged
|
|
16
|
+
* codebases behave exactly as before.
|
|
17
|
+
*/
|
|
18
|
+
function isTestOriginQuery(tags) {
|
|
19
|
+
if (!tags) return false;
|
|
20
|
+
for (const tag of tags) {
|
|
21
|
+
if (tag.key !== "file") continue;
|
|
22
|
+
const originFile = tag.value.split(";")[0]?.trim();
|
|
23
|
+
if (originFile && isTestFilePath(originFile)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a source path looks like a test file. Query Doctor analyzes clients in
|
|
29
|
+
* any language, so this spans the common cross-language conventions rather than
|
|
30
|
+
* assuming JS/TS — directory-scoped (`tests/`, `__tests__/`, `spec/`, `e2e/`,
|
|
31
|
+
* Maven's `src/test/`) and filename-scoped (`*.test.*` / `*.spec.*`, Python
|
|
32
|
+
* `test_*.py` / `*_test.py`, Go `*_test.go`, Ruby `*_test.rb` / `*_spec.rb`,
|
|
33
|
+
* Java `*Test.java`). Anchored on path separators so `testimonials/user.ts` or
|
|
34
|
+
* `latest/report.py` don't get mistaken for tests.
|
|
35
|
+
*/
|
|
36
|
+
function isTestFilePath(path) {
|
|
37
|
+
return TEST_PATH_PATTERNS.some((pattern) => pattern.test(path));
|
|
38
|
+
}
|
|
39
|
+
const TEST_PATH_PATTERNS = [
|
|
40
|
+
/(?:^|\/)(?:tests?|__tests__|specs?|e2e)\//i,
|
|
41
|
+
/(?:^|\/)src\/test\//i,
|
|
42
|
+
/\.(?:test|spec|e2e)\.[cm]?[jt]sx?$/i,
|
|
43
|
+
/(?:^|\/)(?:test_[^/]+|[^/]+_test|conftest)\.py$/i,
|
|
44
|
+
/_test\.go$/i,
|
|
45
|
+
/_(?:test|spec)\.rb$/i,
|
|
46
|
+
/Tests?\.(?:java|kt)$/
|
|
47
|
+
];
|
|
48
|
+
//#endregion
|
|
49
|
+
exports.isTestFilePath = isTestFilePath;
|
|
50
|
+
exports.isTestOriginQuery = isTestOriginQuery;
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=test-origin.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-origin.cjs","names":[],"sources":["../../src/sql/test-origin.ts"],"sourcesContent":["import type { SQLCommenterTag } from \"./analyzer.js\";\n\n/**\n * Whether a query originated from a test file, decided from its SQLCommenter\n * `file` tag.\n *\n * The CI gate should never flag a query that no production route runs — e.g. a\n * read-back assertion in `tests/pg/postgres.test.ts` — yet before this such\n * queries had to be ignored one hash at a time (#3199). SQLCommenter already\n * carries the caller's file (surfaced in query detail per #2585); this reads it\n * so both the analyzer's gate and the server's alert engine can bucket\n * test-origin queries out with a single shared rule instead of drifting apart.\n *\n * Conservative by construction: the decision keys only off the `file` tag's\n * origin path. A query with no `file` tag is never test-origin, so untagged\n * codebases behave exactly as before.\n */\nexport function isTestOriginQuery(\n tags: readonly SQLCommenterTag[] | undefined | null,\n): boolean {\n if (!tags) return false;\n for (const tag of tags) {\n if (tag.key !== \"file\") continue;\n // SQLCommenter can pack a caller stack into one `file` value, `;`-separated\n // and most-specific-first (the frontend renders `files[0]` as the origin).\n // The first entry is the query's own call site, so it decides origin — a\n // production query that merely threads through a test helper deeper in the\n // stack stays gated.\n const originFile = tag.value.split(\";\")[0]?.trim();\n if (originFile && isTestFilePath(originFile)) return true;\n }\n return false;\n}\n\n/**\n * Whether a source path looks like a test file. Query Doctor analyzes clients in\n * any language, so this spans the common cross-language conventions rather than\n * assuming JS/TS — directory-scoped (`tests/`, `__tests__/`, `spec/`, `e2e/`,\n * Maven's `src/test/`) and filename-scoped (`*.test.*` / `*.spec.*`, Python\n * `test_*.py` / `*_test.py`, Go `*_test.go`, Ruby `*_test.rb` / `*_spec.rb`,\n * Java `*Test.java`). Anchored on path separators so `testimonials/user.ts` or\n * `latest/report.py` don't get mistaken for tests.\n */\nexport function isTestFilePath(path: string): boolean {\n return TEST_PATH_PATTERNS.some((pattern) => pattern.test(path));\n}\n\nconst TEST_PATH_PATTERNS: readonly RegExp[] = [\n // A directory segment dedicated to tests, anywhere in the path.\n /(?:^|\\/)(?:tests?|__tests__|specs?|e2e)\\//i,\n // Maven/Gradle layout: src/test/java/...\n /(?:^|\\/)src\\/test\\//i,\n // JS/TS: foo.test.ts, foo.spec.tsx, foo.e2e.mts\n /\\.(?:test|spec|e2e)\\.[cm]?[jt]sx?$/i,\n // Python: test_foo.py, foo_test.py, conftest.py\n /(?:^|\\/)(?:test_[^/]+|[^/]+_test|conftest)\\.py$/i,\n // Go: foo_test.go\n /_test\\.go$/i,\n // Ruby: foo_test.rb, foo_spec.rb\n /_(?:test|spec)\\.rb$/i,\n // Java/Kotlin: FooTest.java, FooTests.kt\n /Tests?\\.(?:java|kt)$/,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBACd,MACS;AACT,KAAI,CAAC,KAAM,QAAO;AAClB,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,IAAI,QAAQ,OAAQ;EAMxB,MAAM,aAAa,IAAI,MAAM,MAAM,IAAI,CAAC,IAAI,MAAM;AAClD,MAAI,cAAc,eAAe,WAAW,CAAE,QAAO;;AAEvD,QAAO;;;;;;;;;;;AAYT,SAAgB,eAAe,MAAuB;AACpD,QAAO,mBAAmB,MAAM,YAAY,QAAQ,KAAK,KAAK,CAAC;;AAGjE,MAAM,qBAAwC;CAE5C;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CACD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { SQLCommenterTag } from "./analyzer.cjs";
|
|
4
|
+
|
|
5
|
+
//#region src/sql/test-origin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Whether a query originated from a test file, decided from its SQLCommenter
|
|
8
|
+
* `file` tag.
|
|
9
|
+
*
|
|
10
|
+
* The CI gate should never flag a query that no production route runs — e.g. a
|
|
11
|
+
* read-back assertion in `tests/pg/postgres.test.ts` — yet before this such
|
|
12
|
+
* queries had to be ignored one hash at a time (#3199). SQLCommenter already
|
|
13
|
+
* carries the caller's file (surfaced in query detail per #2585); this reads it
|
|
14
|
+
* so both the analyzer's gate and the server's alert engine can bucket
|
|
15
|
+
* test-origin queries out with a single shared rule instead of drifting apart.
|
|
16
|
+
*
|
|
17
|
+
* Conservative by construction: the decision keys only off the `file` tag's
|
|
18
|
+
* origin path. A query with no `file` tag is never test-origin, so untagged
|
|
19
|
+
* codebases behave exactly as before.
|
|
20
|
+
*/
|
|
21
|
+
declare function isTestOriginQuery(tags: readonly SQLCommenterTag[] | undefined | null): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a source path looks like a test file. Query Doctor analyzes clients in
|
|
24
|
+
* any language, so this spans the common cross-language conventions rather than
|
|
25
|
+
* assuming JS/TS — directory-scoped (`tests/`, `__tests__/`, `spec/`, `e2e/`,
|
|
26
|
+
* Maven's `src/test/`) and filename-scoped (`*.test.*` / `*.spec.*`, Python
|
|
27
|
+
* `test_*.py` / `*_test.py`, Go `*_test.go`, Ruby `*_test.rb` / `*_spec.rb`,
|
|
28
|
+
* Java `*Test.java`). Anchored on path separators so `testimonials/user.ts` or
|
|
29
|
+
* `latest/report.py` don't get mistaken for tests.
|
|
30
|
+
*/
|
|
31
|
+
declare function isTestFilePath(path: string): boolean;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { isTestFilePath, isTestOriginQuery };
|
|
34
|
+
//# sourceMappingURL=test-origin.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-origin.d.cts","names":[],"sources":["../../src/sql/test-origin.ts"],"mappings":";;;;;;;;AAiBA;;;;;AA0BA;;;;;;;iBA1BgB,iBAAA,CACd,IAAA,WAAe,eAAA;;;;;;;;;;iBAyBD,cAAA,CAAe,IAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { SQLCommenterTag } from "./analyzer.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/sql/test-origin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Whether a query originated from a test file, decided from its SQLCommenter
|
|
8
|
+
* `file` tag.
|
|
9
|
+
*
|
|
10
|
+
* The CI gate should never flag a query that no production route runs — e.g. a
|
|
11
|
+
* read-back assertion in `tests/pg/postgres.test.ts` — yet before this such
|
|
12
|
+
* queries had to be ignored one hash at a time (#3199). SQLCommenter already
|
|
13
|
+
* carries the caller's file (surfaced in query detail per #2585); this reads it
|
|
14
|
+
* so both the analyzer's gate and the server's alert engine can bucket
|
|
15
|
+
* test-origin queries out with a single shared rule instead of drifting apart.
|
|
16
|
+
*
|
|
17
|
+
* Conservative by construction: the decision keys only off the `file` tag's
|
|
18
|
+
* origin path. A query with no `file` tag is never test-origin, so untagged
|
|
19
|
+
* codebases behave exactly as before.
|
|
20
|
+
*/
|
|
21
|
+
declare function isTestOriginQuery(tags: readonly SQLCommenterTag[] | undefined | null): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a source path looks like a test file. Query Doctor analyzes clients in
|
|
24
|
+
* any language, so this spans the common cross-language conventions rather than
|
|
25
|
+
* assuming JS/TS — directory-scoped (`tests/`, `__tests__/`, `spec/`, `e2e/`,
|
|
26
|
+
* Maven's `src/test/`) and filename-scoped (`*.test.*` / `*.spec.*`, Python
|
|
27
|
+
* `test_*.py` / `*_test.py`, Go `*_test.go`, Ruby `*_test.rb` / `*_spec.rb`,
|
|
28
|
+
* Java `*Test.java`). Anchored on path separators so `testimonials/user.ts` or
|
|
29
|
+
* `latest/report.py` don't get mistaken for tests.
|
|
30
|
+
*/
|
|
31
|
+
declare function isTestFilePath(path: string): boolean;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { isTestFilePath, isTestOriginQuery };
|
|
34
|
+
//# sourceMappingURL=test-origin.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-origin.d.mts","names":[],"sources":["../../src/sql/test-origin.ts"],"mappings":";;;;;;;;AAiBA;;;;;AA0BA;;;;;;;iBA1BgB,iBAAA,CACd,IAAA,WAAe,eAAA;;;;;;;;;;iBAyBD,cAAA,CAAe,IAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/sql/test-origin.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether a query originated from a test file, decided from its SQLCommenter
|
|
5
|
+
* `file` tag.
|
|
6
|
+
*
|
|
7
|
+
* The CI gate should never flag a query that no production route runs — e.g. a
|
|
8
|
+
* read-back assertion in `tests/pg/postgres.test.ts` — yet before this such
|
|
9
|
+
* queries had to be ignored one hash at a time (#3199). SQLCommenter already
|
|
10
|
+
* carries the caller's file (surfaced in query detail per #2585); this reads it
|
|
11
|
+
* so both the analyzer's gate and the server's alert engine can bucket
|
|
12
|
+
* test-origin queries out with a single shared rule instead of drifting apart.
|
|
13
|
+
*
|
|
14
|
+
* Conservative by construction: the decision keys only off the `file` tag's
|
|
15
|
+
* origin path. A query with no `file` tag is never test-origin, so untagged
|
|
16
|
+
* codebases behave exactly as before.
|
|
17
|
+
*/
|
|
18
|
+
function isTestOriginQuery(tags) {
|
|
19
|
+
if (!tags) return false;
|
|
20
|
+
for (const tag of tags) {
|
|
21
|
+
if (tag.key !== "file") continue;
|
|
22
|
+
const originFile = tag.value.split(";")[0]?.trim();
|
|
23
|
+
if (originFile && isTestFilePath(originFile)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a source path looks like a test file. Query Doctor analyzes clients in
|
|
29
|
+
* any language, so this spans the common cross-language conventions rather than
|
|
30
|
+
* assuming JS/TS — directory-scoped (`tests/`, `__tests__/`, `spec/`, `e2e/`,
|
|
31
|
+
* Maven's `src/test/`) and filename-scoped (`*.test.*` / `*.spec.*`, Python
|
|
32
|
+
* `test_*.py` / `*_test.py`, Go `*_test.go`, Ruby `*_test.rb` / `*_spec.rb`,
|
|
33
|
+
* Java `*Test.java`). Anchored on path separators so `testimonials/user.ts` or
|
|
34
|
+
* `latest/report.py` don't get mistaken for tests.
|
|
35
|
+
*/
|
|
36
|
+
function isTestFilePath(path) {
|
|
37
|
+
return TEST_PATH_PATTERNS.some((pattern) => pattern.test(path));
|
|
38
|
+
}
|
|
39
|
+
const TEST_PATH_PATTERNS = [
|
|
40
|
+
/(?:^|\/)(?:tests?|__tests__|specs?|e2e)\//i,
|
|
41
|
+
/(?:^|\/)src\/test\//i,
|
|
42
|
+
/\.(?:test|spec|e2e)\.[cm]?[jt]sx?$/i,
|
|
43
|
+
/(?:^|\/)(?:test_[^/]+|[^/]+_test|conftest)\.py$/i,
|
|
44
|
+
/_test\.go$/i,
|
|
45
|
+
/_(?:test|spec)\.rb$/i,
|
|
46
|
+
/Tests?\.(?:java|kt)$/
|
|
47
|
+
];
|
|
48
|
+
//#endregion
|
|
49
|
+
export { isTestFilePath, isTestOriginQuery };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=test-origin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-origin.mjs","names":[],"sources":["../../src/sql/test-origin.ts"],"sourcesContent":["import type { SQLCommenterTag } from \"./analyzer.js\";\n\n/**\n * Whether a query originated from a test file, decided from its SQLCommenter\n * `file` tag.\n *\n * The CI gate should never flag a query that no production route runs — e.g. a\n * read-back assertion in `tests/pg/postgres.test.ts` — yet before this such\n * queries had to be ignored one hash at a time (#3199). SQLCommenter already\n * carries the caller's file (surfaced in query detail per #2585); this reads it\n * so both the analyzer's gate and the server's alert engine can bucket\n * test-origin queries out with a single shared rule instead of drifting apart.\n *\n * Conservative by construction: the decision keys only off the `file` tag's\n * origin path. A query with no `file` tag is never test-origin, so untagged\n * codebases behave exactly as before.\n */\nexport function isTestOriginQuery(\n tags: readonly SQLCommenterTag[] | undefined | null,\n): boolean {\n if (!tags) return false;\n for (const tag of tags) {\n if (tag.key !== \"file\") continue;\n // SQLCommenter can pack a caller stack into one `file` value, `;`-separated\n // and most-specific-first (the frontend renders `files[0]` as the origin).\n // The first entry is the query's own call site, so it decides origin — a\n // production query that merely threads through a test helper deeper in the\n // stack stays gated.\n const originFile = tag.value.split(\";\")[0]?.trim();\n if (originFile && isTestFilePath(originFile)) return true;\n }\n return false;\n}\n\n/**\n * Whether a source path looks like a test file. Query Doctor analyzes clients in\n * any language, so this spans the common cross-language conventions rather than\n * assuming JS/TS — directory-scoped (`tests/`, `__tests__/`, `spec/`, `e2e/`,\n * Maven's `src/test/`) and filename-scoped (`*.test.*` / `*.spec.*`, Python\n * `test_*.py` / `*_test.py`, Go `*_test.go`, Ruby `*_test.rb` / `*_spec.rb`,\n * Java `*Test.java`). Anchored on path separators so `testimonials/user.ts` or\n * `latest/report.py` don't get mistaken for tests.\n */\nexport function isTestFilePath(path: string): boolean {\n return TEST_PATH_PATTERNS.some((pattern) => pattern.test(path));\n}\n\nconst TEST_PATH_PATTERNS: readonly RegExp[] = [\n // A directory segment dedicated to tests, anywhere in the path.\n /(?:^|\\/)(?:tests?|__tests__|specs?|e2e)\\//i,\n // Maven/Gradle layout: src/test/java/...\n /(?:^|\\/)src\\/test\\//i,\n // JS/TS: foo.test.ts, foo.spec.tsx, foo.e2e.mts\n /\\.(?:test|spec|e2e)\\.[cm]?[jt]sx?$/i,\n // Python: test_foo.py, foo_test.py, conftest.py\n /(?:^|\\/)(?:test_[^/]+|[^/]+_test|conftest)\\.py$/i,\n // Go: foo_test.go\n /_test\\.go$/i,\n // Ruby: foo_test.rb, foo_spec.rb\n /_(?:test|spec)\\.rb$/i,\n // Java/Kotlin: FooTest.java, FooTests.kt\n /Tests?\\.(?:java|kt)$/,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBACd,MACS;AACT,KAAI,CAAC,KAAM,QAAO;AAClB,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,IAAI,QAAQ,OAAQ;EAMxB,MAAM,aAAa,IAAI,MAAM,MAAM,IAAI,CAAC,IAAI,MAAM;AAClD,MAAI,cAAc,eAAe,WAAW,CAAE,QAAO;;AAEvD,QAAO;;;;;;;;;;;AAYT,SAAgB,eAAe,MAAuB;AACpD,QAAO,mBAAmB,MAAM,YAAY,QAAQ,KAAK,KAAK,CAAC;;AAGjE,MAAM,qBAAwC;CAE5C;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CACD"}
|