@query-doctor/core 0.10.5 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ const require_nudges = require("./sql/nudges.cjs");
6
6
  const require_analyzer = require("./sql/analyzer.cjs");
7
7
  const require_database = require("./sql/database.cjs");
8
8
  const require_display_query = require("./sql/display-query.cjs");
9
+ const require_normalized_fingerprint = require("./sql/normalized-fingerprint.cjs");
9
10
  const require_indexes = require("./sql/indexes.cjs");
10
11
  const require_builder = require("./sql/builder.cjs");
11
12
  const require_genalgo = require("./optimizer/genalgo.cjs");
@@ -72,4 +73,5 @@ exports.ignoredIdentifier = require_analyzer.ignoredIdentifier;
72
73
  exports.indexCovers = require_index_coverage.indexCovers;
73
74
  exports.isIndexProbablyDroppable = require_indexes.isIndexProbablyDroppable;
74
75
  exports.isIndexSupported = require_indexes.isIndexSupported;
76
+ exports.normalizedFingerprint = require_normalized_fingerprint.normalizedFingerprint;
75
77
  exports.parseNudges = require_nudges.parseNudges;
package/dist/index.d.cts CHANGED
@@ -9,6 +9,7 @@ import { IndexIdentifier, IndexOptimizer, IndexToCreate, OptimizeResult, PROCEED
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
11
  import { CompactSelectListOptions, compactSelectList } from "./sql/display-query.cjs";
12
+ import { FingerprintFn, normalizedFingerprint } from "./sql/normalized-fingerprint.cjs";
12
13
  import { DuplicateIndexGroup, groupDuplicateIndexes, isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.cjs";
13
14
  import { CombinedExport, ExportedQuery, QuerySource, combinedDumpSql, dumpQueriesSql } from "./optimizer/dump.cjs";
14
15
  import { PssRewriter } from "./optimizer/pss-rewriter.cjs";
@@ -19,4 +20,4 @@ import { ActionPlanNudgeQuery, ActionableStep, AffectedQueryCost, CostReduction,
19
20
  import { deriveSentryEnvironment } from "./sentry.cjs";
20
21
  import { ClientApi, ConnectionMode, ExtensionPresence, IndexDefinition, RepoConfig, ServerApi, UnauthenticatedServerApi } from "./websocket-server.cjs";
21
22
  import { QueryFinding, QueryFindingCode, QueryFindingSeverity, analyzeQueryFindings } from "./findings/query-findings.cjs";
22
- 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, 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, parseNudges };
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 };
package/dist/index.d.mts CHANGED
@@ -9,6 +9,7 @@ import { IndexIdentifier, IndexOptimizer, IndexToCreate, OptimizeResult, PROCEED
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
11
  import { CompactSelectListOptions, compactSelectList } from "./sql/display-query.mjs";
12
+ import { FingerprintFn, normalizedFingerprint } from "./sql/normalized-fingerprint.mjs";
12
13
  import { DuplicateIndexGroup, groupDuplicateIndexes, isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.mjs";
13
14
  import { CombinedExport, ExportedQuery, QuerySource, combinedDumpSql, dumpQueriesSql } from "./optimizer/dump.mjs";
14
15
  import { PssRewriter } from "./optimizer/pss-rewriter.mjs";
@@ -19,4 +20,4 @@ import { ActionPlanNudgeQuery, ActionableStep, AffectedQueryCost, CostReduction,
19
20
  import { deriveSentryEnvironment } from "./sentry.mjs";
20
21
  import { ClientApi, ConnectionMode, ExtensionPresence, IndexDefinition, RepoConfig, ServerApi, UnauthenticatedServerApi } from "./websocket-server.mjs";
21
22
  import { QueryFinding, QueryFindingCode, QueryFindingSeverity, analyzeQueryFindings } from "./findings/query-findings.mjs";
22
- 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, 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, parseNudges };
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 };
package/dist/index.mjs CHANGED
@@ -5,6 +5,7 @@ import { parseNudges } from "./sql/nudges.mjs";
5
5
  import { Analyzer, ignoredIdentifier } from "./sql/analyzer.mjs";
6
6
  import { PostgresExplainStageSchema, PostgresVersion, dropIndex } from "./sql/database.mjs";
7
7
  import { compactSelectList } from "./sql/display-query.mjs";
8
+ import { normalizedFingerprint } from "./sql/normalized-fingerprint.mjs";
8
9
  import { groupDuplicateIndexes, isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.mjs";
9
10
  import { PostgresQueryBuilder } from "./sql/builder.mjs";
10
11
  import { IndexOptimizer, PROCEED, SKIP } from "./optimizer/genalgo.mjs";
@@ -17,4 +18,4 @@ import { buildActionPlan } from "./action-plan/build-action-plan.mjs";
17
18
  import { deriveSentryEnvironment } from "./sentry.mjs";
18
19
  import { OptimizedQuery, RecentQuery } from "./query.mjs";
19
20
  import { analyzeQueryFindings } from "./findings/query-findings.mjs";
20
- 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, parseNudges };
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 };
package/dist/query.cjs CHANGED
@@ -21,7 +21,7 @@ const RecentQuery = zod.z.object({
21
21
  tags: zod.z.array(zod.z.custom()),
22
22
  nudges: zod.z.array(zod.z.custom()).optional().default([]),
23
23
  hash: zod.z.string(),
24
- seenAt: zod.z.number(),
24
+ firstSeen: zod.z.number().optional(),
25
25
  optimization: zod.z.custom().optional(),
26
26
  explainPlan: require_database.PostgresExplainStageSchema.optional()
27
27
  });
@@ -1 +1 @@
1
- {"version":3,"file":"query.cjs","names":["z","PostgresExplainStageSchema"],"sources":["../src/query.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type {\n DiscoveredColumnReference,\n SQLCommenterTag,\n TableReference,\n} from \"./sql/analyzer.js\";\nimport {\n PostgresExplainStageSchema,\n type PostgresExplainStage,\n} from \"./sql/database.js\";\nimport type { Nudge } from \"./sql/nudges.js\";\n\nexport type IndexRecommendation = {\n name?: string;\n schema: string;\n table: string;\n columns: Array<{\n schema: string;\n table: string;\n column: string;\n sort?: unknown;\n where?: unknown;\n }>;\n where?: string;\n definition: string;\n};\n\nexport type LiveQueryOptimization =\n | { state: \"waiting\" }\n | { state: \"optimizing\" }\n | { state: \"not_supported\"; reason: string }\n | {\n state: \"improvements_available\";\n cost: number;\n optimizedCost: number;\n costReductionPercentage: number;\n indexRecommendations: IndexRecommendation[];\n triedIndexNames?: Record<string, string>;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n optimizedExplainPlan?: PostgresExplainStage;\n }\n | {\n state: \"no_improvement_found\";\n cost: number;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n }\n | { state: \"timeout\"; cost?: number }\n | { state: \"error\"; error: string };\n\nexport type QueryHash = string;\n\nexport const RecentQuery = z.object({\n formattedQuery: z.string(),\n displayQuery: z.string().optional(),\n username: z.string(),\n query: z.string(),\n meanTime: z.number(),\n calls: z.string(),\n rows: z.string(),\n topLevel: z.boolean(),\n isSystemQuery: z.boolean(),\n isSelectQuery: z.boolean(),\n isIntrospection: z.boolean(),\n isTargetlessSelectQuery: z.boolean(),\n tableReferences: z.array(z.custom<TableReference>()),\n columnReferences: z.array(z.custom<DiscoveredColumnReference>()),\n tags: z.array(z.custom<SQLCommenterTag>()),\n nudges: z.array(z.custom<Nudge>()).optional().default([]),\n hash: z.string(),\n seenAt: z.number(),\n optimization: z.custom<LiveQueryOptimization>().optional(),\n explainPlan: PostgresExplainStageSchema.optional(),\n});\n\nexport type RecentQuery = z.infer<typeof RecentQuery>;\n\nexport const OptimizedQuery = RecentQuery.extend({\n optimization: z.custom<LiveQueryOptimization>(),\n});\n\nexport type OptimizedQuery = z.infer<typeof OptimizedQuery>;\n\nexport type OptimizationResult = {\n hash: QueryHash;\n optimization: LiveQueryOptimization;\n};\n"],"mappings":";;;;;AAqDA,MAAa,cAAcA,IAAAA,EAAE,OAAO;CAClC,gBAAgBA,IAAAA,EAAE,QAAQ;CAC1B,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,UAAUA,IAAAA,EAAE,QAAQ;CACpB,OAAOA,IAAAA,EAAE,QAAQ;CACjB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,OAAOA,IAAAA,EAAE,QAAQ;CACjB,MAAMA,IAAAA,EAAE,QAAQ;CAChB,UAAUA,IAAAA,EAAE,SAAS;CACrB,eAAeA,IAAAA,EAAE,SAAS;CAC1B,eAAeA,IAAAA,EAAE,SAAS;CAC1B,iBAAiBA,IAAAA,EAAE,SAAS;CAC5B,yBAAyBA,IAAAA,EAAE,SAAS;CACpC,iBAAiBA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAwB,CAAC;CACpD,kBAAkBA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAmC,CAAC;CAChE,MAAMA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAyB,CAAC;CAC1C,QAAQA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAe,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;CACzD,MAAMA,IAAAA,EAAE,QAAQ;CAChB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,cAAcA,IAAAA,EAAE,QAA+B,CAAC,UAAU;CAC1D,aAAaC,iBAAAA,2BAA2B,UAAU;CACnD,CAAC;AAIF,MAAa,iBAAiB,YAAY,OAAO,EAC/C,cAAcD,IAAAA,EAAE,QAA+B,EAChD,CAAC"}
1
+ {"version":3,"file":"query.cjs","names":["z","PostgresExplainStageSchema"],"sources":["../src/query.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type {\n DiscoveredColumnReference,\n SQLCommenterTag,\n TableReference,\n} from \"./sql/analyzer.js\";\nimport {\n PostgresExplainStageSchema,\n type PostgresExplainStage,\n} from \"./sql/database.js\";\nimport type { Nudge } from \"./sql/nudges.js\";\n\nexport type IndexRecommendation = {\n name?: string;\n schema: string;\n table: string;\n columns: Array<{\n schema: string;\n table: string;\n column: string;\n sort?: unknown;\n where?: unknown;\n }>;\n where?: string;\n definition: string;\n};\n\nexport type LiveQueryOptimization =\n | { state: \"waiting\" }\n | { state: \"optimizing\" }\n | { state: \"not_supported\"; reason: string }\n | {\n state: \"improvements_available\";\n cost: number;\n optimizedCost: number;\n costReductionPercentage: number;\n indexRecommendations: IndexRecommendation[];\n triedIndexNames?: Record<string, string>;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n optimizedExplainPlan?: PostgresExplainStage;\n }\n | {\n state: \"no_improvement_found\";\n cost: number;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n }\n | { state: \"timeout\"; cost?: number }\n | { state: \"error\"; error: string };\n\nexport type QueryHash = string;\n\nexport const RecentQuery = z.object({\n formattedQuery: z.string(),\n displayQuery: z.string().optional(),\n username: z.string(),\n query: z.string(),\n meanTime: z.number(),\n calls: z.string(),\n rows: z.string(),\n topLevel: z.boolean(),\n isSystemQuery: z.boolean(),\n isSelectQuery: z.boolean(),\n isIntrospection: z.boolean(),\n isTargetlessSelectQuery: z.boolean(),\n tableReferences: z.array(z.custom<TableReference>()),\n columnReferences: z.array(z.custom<DiscoveredColumnReference>()),\n tags: z.array(z.custom<SQLCommenterTag>()),\n nudges: z.array(z.custom<Nudge>()).optional().default([]),\n hash: z.string(),\n firstSeen: z.number().optional(),\n optimization: z.custom<LiveQueryOptimization>().optional(),\n explainPlan: PostgresExplainStageSchema.optional(),\n});\n\nexport type RecentQuery = z.infer<typeof RecentQuery>;\n\nexport const OptimizedQuery = RecentQuery.extend({\n optimization: z.custom<LiveQueryOptimization>(),\n});\n\nexport type OptimizedQuery = z.infer<typeof OptimizedQuery>;\n\nexport type OptimizationResult = {\n hash: QueryHash;\n optimization: LiveQueryOptimization;\n};\n"],"mappings":";;;;;AAqDA,MAAa,cAAcA,IAAAA,EAAE,OAAO;CAClC,gBAAgBA,IAAAA,EAAE,QAAQ;CAC1B,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,UAAUA,IAAAA,EAAE,QAAQ;CACpB,OAAOA,IAAAA,EAAE,QAAQ;CACjB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,OAAOA,IAAAA,EAAE,QAAQ;CACjB,MAAMA,IAAAA,EAAE,QAAQ;CAChB,UAAUA,IAAAA,EAAE,SAAS;CACrB,eAAeA,IAAAA,EAAE,SAAS;CAC1B,eAAeA,IAAAA,EAAE,SAAS;CAC1B,iBAAiBA,IAAAA,EAAE,SAAS;CAC5B,yBAAyBA,IAAAA,EAAE,SAAS;CACpC,iBAAiBA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAwB,CAAC;CACpD,kBAAkBA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAmC,CAAC;CAChE,MAAMA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAyB,CAAC;CAC1C,QAAQA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAe,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;CACzD,MAAMA,IAAAA,EAAE,QAAQ;CAChB,WAAWA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CAChC,cAAcA,IAAAA,EAAE,QAA+B,CAAC,UAAU;CAC1D,aAAaC,iBAAAA,2BAA2B,UAAU;CACnD,CAAC;AAIF,MAAa,iBAAiB,YAAY,OAAO,EAC/C,cAAcD,IAAAA,EAAE,QAA+B,EAChD,CAAC"}
package/dist/query.d.cts CHANGED
@@ -68,7 +68,7 @@ declare const RecentQuery: z.ZodObject<{
68
68
  tags: z.ZodArray<z.ZodCustom<SQLCommenterTag, SQLCommenterTag>>;
69
69
  nudges: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodCustom<Nudge, Nudge>>>>;
70
70
  hash: z.ZodString;
71
- seenAt: z.ZodNumber;
71
+ firstSeen: z.ZodOptional<z.ZodNumber>;
72
72
  optimization: z.ZodOptional<z.ZodCustom<LiveQueryOptimization, LiveQueryOptimization>>;
73
73
  explainPlan: z.ZodOptional<z.ZodType<PostgresExplainStage, unknown, z.core.$ZodTypeInternals<PostgresExplainStage, unknown>>>;
74
74
  }, z.core.$strip>;
@@ -91,7 +91,7 @@ declare const OptimizedQuery: z.ZodObject<{
91
91
  tags: z.ZodArray<z.ZodCustom<SQLCommenterTag, SQLCommenterTag>>;
92
92
  nudges: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodCustom<Nudge, Nudge>>>>;
93
93
  hash: z.ZodString;
94
- seenAt: z.ZodNumber;
94
+ firstSeen: z.ZodOptional<z.ZodNumber>;
95
95
  explainPlan: z.ZodOptional<z.ZodType<PostgresExplainStage, unknown, z.core.$ZodTypeInternals<PostgresExplainStage, unknown>>>;
96
96
  optimization: z.ZodCustom<LiveQueryOptimization, LiveQueryOptimization>;
97
97
  }, z.core.$strip>;
package/dist/query.d.mts CHANGED
@@ -68,7 +68,7 @@ declare const RecentQuery: z.ZodObject<{
68
68
  tags: z.ZodArray<z.ZodCustom<SQLCommenterTag, SQLCommenterTag>>;
69
69
  nudges: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodCustom<Nudge, Nudge>>>>;
70
70
  hash: z.ZodString;
71
- seenAt: z.ZodNumber;
71
+ firstSeen: z.ZodOptional<z.ZodNumber>;
72
72
  optimization: z.ZodOptional<z.ZodCustom<LiveQueryOptimization, LiveQueryOptimization>>;
73
73
  explainPlan: z.ZodOptional<z.ZodType<PostgresExplainStage, unknown, z.core.$ZodTypeInternals<PostgresExplainStage, unknown>>>;
74
74
  }, z.core.$strip>;
@@ -91,7 +91,7 @@ declare const OptimizedQuery: z.ZodObject<{
91
91
  tags: z.ZodArray<z.ZodCustom<SQLCommenterTag, SQLCommenterTag>>;
92
92
  nudges: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodCustom<Nudge, Nudge>>>>;
93
93
  hash: z.ZodString;
94
- seenAt: z.ZodNumber;
94
+ firstSeen: z.ZodOptional<z.ZodNumber>;
95
95
  explainPlan: z.ZodOptional<z.ZodType<PostgresExplainStage, unknown, z.core.$ZodTypeInternals<PostgresExplainStage, unknown>>>;
96
96
  optimization: z.ZodCustom<LiveQueryOptimization, LiveQueryOptimization>;
97
97
  }, z.core.$strip>;
package/dist/query.mjs CHANGED
@@ -20,7 +20,7 @@ const RecentQuery = z.object({
20
20
  tags: z.array(z.custom()),
21
21
  nudges: z.array(z.custom()).optional().default([]),
22
22
  hash: z.string(),
23
- seenAt: z.number(),
23
+ firstSeen: z.number().optional(),
24
24
  optimization: z.custom().optional(),
25
25
  explainPlan: PostgresExplainStageSchema.optional()
26
26
  });
@@ -1 +1 @@
1
- {"version":3,"file":"query.mjs","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type {\n DiscoveredColumnReference,\n SQLCommenterTag,\n TableReference,\n} from \"./sql/analyzer.js\";\nimport {\n PostgresExplainStageSchema,\n type PostgresExplainStage,\n} from \"./sql/database.js\";\nimport type { Nudge } from \"./sql/nudges.js\";\n\nexport type IndexRecommendation = {\n name?: string;\n schema: string;\n table: string;\n columns: Array<{\n schema: string;\n table: string;\n column: string;\n sort?: unknown;\n where?: unknown;\n }>;\n where?: string;\n definition: string;\n};\n\nexport type LiveQueryOptimization =\n | { state: \"waiting\" }\n | { state: \"optimizing\" }\n | { state: \"not_supported\"; reason: string }\n | {\n state: \"improvements_available\";\n cost: number;\n optimizedCost: number;\n costReductionPercentage: number;\n indexRecommendations: IndexRecommendation[];\n triedIndexNames?: Record<string, string>;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n optimizedExplainPlan?: PostgresExplainStage;\n }\n | {\n state: \"no_improvement_found\";\n cost: number;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n }\n | { state: \"timeout\"; cost?: number }\n | { state: \"error\"; error: string };\n\nexport type QueryHash = string;\n\nexport const RecentQuery = z.object({\n formattedQuery: z.string(),\n displayQuery: z.string().optional(),\n username: z.string(),\n query: z.string(),\n meanTime: z.number(),\n calls: z.string(),\n rows: z.string(),\n topLevel: z.boolean(),\n isSystemQuery: z.boolean(),\n isSelectQuery: z.boolean(),\n isIntrospection: z.boolean(),\n isTargetlessSelectQuery: z.boolean(),\n tableReferences: z.array(z.custom<TableReference>()),\n columnReferences: z.array(z.custom<DiscoveredColumnReference>()),\n tags: z.array(z.custom<SQLCommenterTag>()),\n nudges: z.array(z.custom<Nudge>()).optional().default([]),\n hash: z.string(),\n seenAt: z.number(),\n optimization: z.custom<LiveQueryOptimization>().optional(),\n explainPlan: PostgresExplainStageSchema.optional(),\n});\n\nexport type RecentQuery = z.infer<typeof RecentQuery>;\n\nexport const OptimizedQuery = RecentQuery.extend({\n optimization: z.custom<LiveQueryOptimization>(),\n});\n\nexport type OptimizedQuery = z.infer<typeof OptimizedQuery>;\n\nexport type OptimizationResult = {\n hash: QueryHash;\n optimization: LiveQueryOptimization;\n};\n"],"mappings":";;;;AAqDA,MAAa,cAAc,EAAE,OAAO;CAClC,gBAAgB,EAAE,QAAQ;CAC1B,cAAc,EAAE,QAAQ,CAAC,UAAU;CACnC,UAAU,EAAE,QAAQ;CACpB,OAAO,EAAE,QAAQ;CACjB,UAAU,EAAE,QAAQ;CACpB,OAAO,EAAE,QAAQ;CACjB,MAAM,EAAE,QAAQ;CAChB,UAAU,EAAE,SAAS;CACrB,eAAe,EAAE,SAAS;CAC1B,eAAe,EAAE,SAAS;CAC1B,iBAAiB,EAAE,SAAS;CAC5B,yBAAyB,EAAE,SAAS;CACpC,iBAAiB,EAAE,MAAM,EAAE,QAAwB,CAAC;CACpD,kBAAkB,EAAE,MAAM,EAAE,QAAmC,CAAC;CAChE,MAAM,EAAE,MAAM,EAAE,QAAyB,CAAC;CAC1C,QAAQ,EAAE,MAAM,EAAE,QAAe,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;CACzD,MAAM,EAAE,QAAQ;CAChB,QAAQ,EAAE,QAAQ;CAClB,cAAc,EAAE,QAA+B,CAAC,UAAU;CAC1D,aAAa,2BAA2B,UAAU;CACnD,CAAC;AAIF,MAAa,iBAAiB,YAAY,OAAO,EAC/C,cAAc,EAAE,QAA+B,EAChD,CAAC"}
1
+ {"version":3,"file":"query.mjs","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type {\n DiscoveredColumnReference,\n SQLCommenterTag,\n TableReference,\n} from \"./sql/analyzer.js\";\nimport {\n PostgresExplainStageSchema,\n type PostgresExplainStage,\n} from \"./sql/database.js\";\nimport type { Nudge } from \"./sql/nudges.js\";\n\nexport type IndexRecommendation = {\n name?: string;\n schema: string;\n table: string;\n columns: Array<{\n schema: string;\n table: string;\n column: string;\n sort?: unknown;\n where?: unknown;\n }>;\n where?: string;\n definition: string;\n};\n\nexport type LiveQueryOptimization =\n | { state: \"waiting\" }\n | { state: \"optimizing\" }\n | { state: \"not_supported\"; reason: string }\n | {\n state: \"improvements_available\";\n cost: number;\n optimizedCost: number;\n costReductionPercentage: number;\n indexRecommendations: IndexRecommendation[];\n triedIndexNames?: Record<string, string>;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n optimizedExplainPlan?: PostgresExplainStage;\n }\n | {\n state: \"no_improvement_found\";\n cost: number;\n indexesUsed: string[];\n explainPlan?: PostgresExplainStage;\n }\n | { state: \"timeout\"; cost?: number }\n | { state: \"error\"; error: string };\n\nexport type QueryHash = string;\n\nexport const RecentQuery = z.object({\n formattedQuery: z.string(),\n displayQuery: z.string().optional(),\n username: z.string(),\n query: z.string(),\n meanTime: z.number(),\n calls: z.string(),\n rows: z.string(),\n topLevel: z.boolean(),\n isSystemQuery: z.boolean(),\n isSelectQuery: z.boolean(),\n isIntrospection: z.boolean(),\n isTargetlessSelectQuery: z.boolean(),\n tableReferences: z.array(z.custom<TableReference>()),\n columnReferences: z.array(z.custom<DiscoveredColumnReference>()),\n tags: z.array(z.custom<SQLCommenterTag>()),\n nudges: z.array(z.custom<Nudge>()).optional().default([]),\n hash: z.string(),\n firstSeen: z.number().optional(),\n optimization: z.custom<LiveQueryOptimization>().optional(),\n explainPlan: PostgresExplainStageSchema.optional(),\n});\n\nexport type RecentQuery = z.infer<typeof RecentQuery>;\n\nexport const OptimizedQuery = RecentQuery.extend({\n optimization: z.custom<LiveQueryOptimization>(),\n});\n\nexport type OptimizedQuery = z.infer<typeof OptimizedQuery>;\n\nexport type OptimizationResult = {\n hash: QueryHash;\n optimization: LiveQueryOptimization;\n};\n"],"mappings":";;;;AAqDA,MAAa,cAAc,EAAE,OAAO;CAClC,gBAAgB,EAAE,QAAQ;CAC1B,cAAc,EAAE,QAAQ,CAAC,UAAU;CACnC,UAAU,EAAE,QAAQ;CACpB,OAAO,EAAE,QAAQ;CACjB,UAAU,EAAE,QAAQ;CACpB,OAAO,EAAE,QAAQ;CACjB,MAAM,EAAE,QAAQ;CAChB,UAAU,EAAE,SAAS;CACrB,eAAe,EAAE,SAAS;CAC1B,eAAe,EAAE,SAAS;CAC1B,iBAAiB,EAAE,SAAS;CAC5B,yBAAyB,EAAE,SAAS;CACpC,iBAAiB,EAAE,MAAM,EAAE,QAAwB,CAAC;CACpD,kBAAkB,EAAE,MAAM,EAAE,QAAmC,CAAC;CAChE,MAAM,EAAE,MAAM,EAAE,QAAyB,CAAC;CAC1C,QAAQ,EAAE,MAAM,EAAE,QAAe,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;CACzD,MAAM,EAAE,QAAQ;CAChB,WAAW,EAAE,QAAQ,CAAC,UAAU;CAChC,cAAc,EAAE,QAA+B,CAAC,UAAU;CAC1D,aAAa,2BAA2B,UAAU;CACnD,CAAC;AAIF,MAAa,iBAAiB,YAAY,OAAO,EAC/C,cAAc,EAAE,QAA+B,EAChD,CAAC"}
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ require("../_virtual/_rolldown/runtime.cjs");
3
+ const require_ast_utils = require("./ast-utils.cjs");
4
+ const require_walker = require("./walker.cjs");
5
+ let pgsql_deparser = require("pgsql-deparser");
6
+ //#region src/sql/normalized-fingerprint.ts
7
+ const SELECT_LIST_PLACEHOLDER = { ResTarget: {
8
+ val: { A_Const: {
9
+ ival: { ival: 1 },
10
+ location: -1
11
+ } },
12
+ location: -1
13
+ } };
14
+ /**
15
+ * A "bare column" target: any `ColumnRef`, including `a`, qualified `t.a`, and
16
+ * the wildcards `*` and `t.*`. `count(*)`, `a + b`, literals, and `AS` aliases
17
+ * are not column refs and survive normalization.
18
+ */
19
+ function isBareColumn(target) {
20
+ if (!require_ast_utils.is(target, "ResTarget")) return false;
21
+ const val = target.ResTarget.val;
22
+ if (!val) return false;
23
+ return require_ast_utils.is(val, "ColumnRef");
24
+ }
25
+ /**
26
+ * Set-operation arms (`larg`/`rarg`) are inlined `SelectStmt` bodies rather
27
+ * than wrapped `{ SelectStmt }` nodes, so `Walker.traverse` never emits them.
28
+ * Drill into them explicitly; they can nest (`a UNION b UNION c`).
29
+ */
30
+ function normalizeSelect(select) {
31
+ const targetList = select.targetList;
32
+ if (targetList) {
33
+ const survivors = targetList.filter((t) => !isBareColumn(t));
34
+ if (survivors.length > 1) {
35
+ const keyed = survivors.map((t) => ({
36
+ key: (0, pgsql_deparser.deparseSync)(t),
37
+ t
38
+ }));
39
+ keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
40
+ select.targetList = keyed.map((k) => k.t);
41
+ } else select.targetList = survivors.length ? survivors : [SELECT_LIST_PLACEHOLDER];
42
+ }
43
+ if (select.larg) normalizeSelect(select.larg);
44
+ if (select.rarg) normalizeSelect(select.rarg);
45
+ }
46
+ /**
47
+ * Fingerprint a query while normalizing every SELECT target list in the tree
48
+ * so that the presence and order of bare column references don't affect the
49
+ * result. Applies to the top-level SELECT as well as SELECTs nested in CTEs,
50
+ * subqueries, and set-operation arms. A change to the set of
51
+ * aggregates/expressions, the WHERE, the FROM, etc. still moves it.
52
+ *
53
+ * Takes the already-parsed AST and an injected {@link FingerprintFn} so core
54
+ * needs no parser or fingerprint runtime. The AST is cloned before mutation.
55
+ */
56
+ async function normalizedFingerprint(ast, fingerprint) {
57
+ const cloned = JSON.parse(JSON.stringify(ast));
58
+ for (const raw of cloned.stmts ?? []) {
59
+ if (!raw.stmt) continue;
60
+ require_walker.Walker.traverse(raw.stmt, (node) => {
61
+ if (require_ast_utils.is(node, "SelectStmt")) normalizeSelect(node.SelectStmt);
62
+ });
63
+ }
64
+ return fingerprint((0, pgsql_deparser.deparseSync)(cloned));
65
+ }
66
+ //#endregion
67
+ exports.normalizedFingerprint = normalizedFingerprint;
68
+
69
+ //# sourceMappingURL=normalized-fingerprint.cjs.map
@@ -0,0 +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 ? survivors : [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,SAAS,YAAY,CAAC,wBAAwB;;AAGhF,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"}
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { ParseResult } from "@pgsql/types";
4
+
5
+ //#region src/sql/normalized-fingerprint.d.ts
6
+ /**
7
+ * A function that fingerprints a SQL string. Injected so this module stays
8
+ * free of a fingerprint runtime dependency, mirroring how `Analyzer` accepts
9
+ * an injected parser. In practice this is `fingerprint` from
10
+ * `@libpg-query/parser`.
11
+ */
12
+ type FingerprintFn = (query: string) => Promise<string>;
13
+ /**
14
+ * Fingerprint a query while normalizing every SELECT target list in the tree
15
+ * so that the presence and order of bare column references don't affect the
16
+ * result. Applies to the top-level SELECT as well as SELECTs nested in CTEs,
17
+ * subqueries, and set-operation arms. A change to the set of
18
+ * aggregates/expressions, the WHERE, the FROM, etc. still moves it.
19
+ *
20
+ * Takes the already-parsed AST and an injected {@link FingerprintFn} so core
21
+ * needs no parser or fingerprint runtime. The AST is cloned before mutation.
22
+ */
23
+ declare function normalizedFingerprint(ast: ParseResult, fingerprint: FingerprintFn): Promise<string>;
24
+ //#endregion
25
+ export { FingerprintFn, normalizedFingerprint };
26
+ //# sourceMappingURL=normalized-fingerprint.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalized-fingerprint.d.cts","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"mappings":";;;;;;;;AAWA;;;KAAY,aAAA,IAAiB,KAAA,aAAkB,OAAA;;AAsD/C;;;;;;;;;iBAAsB,qBAAA,CACpB,GAAA,EAAK,WAAA,EACL,WAAA,EAAa,aAAA,GACZ,OAAA"}
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { ParseResult } from "@pgsql/types";
4
+
5
+ //#region src/sql/normalized-fingerprint.d.ts
6
+ /**
7
+ * A function that fingerprints a SQL string. Injected so this module stays
8
+ * free of a fingerprint runtime dependency, mirroring how `Analyzer` accepts
9
+ * an injected parser. In practice this is `fingerprint` from
10
+ * `@libpg-query/parser`.
11
+ */
12
+ type FingerprintFn = (query: string) => Promise<string>;
13
+ /**
14
+ * Fingerprint a query while normalizing every SELECT target list in the tree
15
+ * so that the presence and order of bare column references don't affect the
16
+ * result. Applies to the top-level SELECT as well as SELECTs nested in CTEs,
17
+ * subqueries, and set-operation arms. A change to the set of
18
+ * aggregates/expressions, the WHERE, the FROM, etc. still moves it.
19
+ *
20
+ * Takes the already-parsed AST and an injected {@link FingerprintFn} so core
21
+ * needs no parser or fingerprint runtime. The AST is cloned before mutation.
22
+ */
23
+ declare function normalizedFingerprint(ast: ParseResult, fingerprint: FingerprintFn): Promise<string>;
24
+ //#endregion
25
+ export { FingerprintFn, normalizedFingerprint };
26
+ //# sourceMappingURL=normalized-fingerprint.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalized-fingerprint.d.mts","names":[],"sources":["../../src/sql/normalized-fingerprint.ts"],"mappings":";;;;;;;;AAWA;;;KAAY,aAAA,IAAiB,KAAA,aAAkB,OAAA;;AAsD/C;;;;;;;;;iBAAsB,qBAAA,CACpB,GAAA,EAAK,WAAA,EACL,WAAA,EAAa,aAAA,GACZ,OAAA"}
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { is } from "./ast-utils.mjs";
3
+ import { Walker } from "./walker.mjs";
4
+ import { deparseSync } from "pgsql-deparser";
5
+ //#region src/sql/normalized-fingerprint.ts
6
+ const SELECT_LIST_PLACEHOLDER = { ResTarget: {
7
+ val: { A_Const: {
8
+ ival: { ival: 1 },
9
+ location: -1
10
+ } },
11
+ location: -1
12
+ } };
13
+ /**
14
+ * A "bare column" target: any `ColumnRef`, including `a`, qualified `t.a`, and
15
+ * the wildcards `*` and `t.*`. `count(*)`, `a + b`, literals, and `AS` aliases
16
+ * are not column refs and survive normalization.
17
+ */
18
+ function isBareColumn(target) {
19
+ if (!is(target, "ResTarget")) return false;
20
+ const val = target.ResTarget.val;
21
+ if (!val) return false;
22
+ return is(val, "ColumnRef");
23
+ }
24
+ /**
25
+ * Set-operation arms (`larg`/`rarg`) are inlined `SelectStmt` bodies rather
26
+ * than wrapped `{ SelectStmt }` nodes, so `Walker.traverse` never emits them.
27
+ * Drill into them explicitly; they can nest (`a UNION b UNION c`).
28
+ */
29
+ function normalizeSelect(select) {
30
+ const targetList = select.targetList;
31
+ if (targetList) {
32
+ const survivors = targetList.filter((t) => !isBareColumn(t));
33
+ if (survivors.length > 1) {
34
+ const keyed = survivors.map((t) => ({
35
+ key: deparseSync(t),
36
+ t
37
+ }));
38
+ keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
39
+ select.targetList = keyed.map((k) => k.t);
40
+ } else select.targetList = survivors.length ? survivors : [SELECT_LIST_PLACEHOLDER];
41
+ }
42
+ if (select.larg) normalizeSelect(select.larg);
43
+ if (select.rarg) normalizeSelect(select.rarg);
44
+ }
45
+ /**
46
+ * Fingerprint a query while normalizing every SELECT target list in the tree
47
+ * so that the presence and order of bare column references don't affect the
48
+ * result. Applies to the top-level SELECT as well as SELECTs nested in CTEs,
49
+ * subqueries, and set-operation arms. A change to the set of
50
+ * aggregates/expressions, the WHERE, the FROM, etc. still moves it.
51
+ *
52
+ * Takes the already-parsed AST and an injected {@link FingerprintFn} so core
53
+ * needs no parser or fingerprint runtime. The AST is cloned before mutation.
54
+ */
55
+ async function normalizedFingerprint(ast, fingerprint) {
56
+ const cloned = JSON.parse(JSON.stringify(ast));
57
+ for (const raw of cloned.stmts ?? []) {
58
+ if (!raw.stmt) continue;
59
+ Walker.traverse(raw.stmt, (node) => {
60
+ if (is(node, "SelectStmt")) normalizeSelect(node.SelectStmt);
61
+ });
62
+ }
63
+ return fingerprint(deparseSync(cloned));
64
+ }
65
+ //#endregion
66
+ export { normalizedFingerprint };
67
+
68
+ //# sourceMappingURL=normalized-fingerprint.mjs.map
@@ -0,0 +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 ? survivors : [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,SAAS,YAAY,CAAC,wBAAwB;;AAGhF,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@query-doctor/core",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "private": false,
5
5
  "description": "Core logic for Query Doctor",
6
6
  "license": "",