@query-doctor/core 0.8.7 → 0.8.8-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -4,7 +4,7 @@ import { PgIdentifier } from "./sql/pg-identifier.cjs";
4
4
  import { Parameter, PostgresQueryBuilder, PostgresQueryBuilderCommand } from "./sql/builder.cjs";
5
5
  import { Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, dropIndex } from "./sql/database.cjs";
6
6
  import { ColumnMetadata, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexOrder, IndexedTable, Path, SerializeResult, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableStats } from "./optimizer/statistics.cjs";
7
- import { IndexIdentifier, IndexOptimizer, IndexRecommendation, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP } from "./optimizer/genalgo.cjs";
7
+ import { IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP } from "./optimizer/genalgo.cjs";
8
8
  import { Nudge, parseNudges } from "./sql/nudges.cjs";
9
9
  import { AnalysisResult, Analyzer, DatabaseDriver, DiscoveredColumnReference, JsonbOperator, Parser, SQLCommenterExtraction, SQLCommenterTag, SortContext, StatementType, TableReference, ignoredIdentifier } from "./sql/analyzer.cjs";
10
10
  import { CompactSelectListOptions, compactSelectList } from "./sql/display-query.cjs";
@@ -12,4 +12,4 @@ import { isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.cjs";
12
12
  import { CombinedExport, ExportedQuery, QuerySource, combinedDumpSql, dumpQueriesSql } from "./optimizer/dump.cjs";
13
13
  import { PssRewriter } from "./optimizer/pss-rewriter.cjs";
14
14
  import { deriveSentryEnvironment } from "./sentry.cjs";
15
- export { AnalysisResult, Analyzer, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexIdentifier, IndexOptimizer, IndexOrder, IndexRecommendation, IndexedTable, JsonbOperator, Nudge, OptimizeResult, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QuerySource, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
15
+ export { AnalysisResult, Analyzer, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexIdentifier, IndexOptimizer, IndexOrder, IndexRecommendation, IndexToCreate, IndexedTable, JsonbOperator, Nudge, OptimizeResult, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QuerySource, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
package/dist/index.d.mts CHANGED
@@ -4,7 +4,7 @@ import { PgIdentifier } from "./sql/pg-identifier.mjs";
4
4
  import { Parameter, PostgresQueryBuilder, PostgresQueryBuilderCommand } from "./sql/builder.mjs";
5
5
  import { Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, dropIndex } from "./sql/database.mjs";
6
6
  import { ColumnMetadata, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexOrder, IndexedTable, Path, SerializeResult, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableStats } from "./optimizer/statistics.mjs";
7
- import { IndexIdentifier, IndexOptimizer, IndexRecommendation, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP } from "./optimizer/genalgo.mjs";
7
+ import { IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP } from "./optimizer/genalgo.mjs";
8
8
  import { Nudge, parseNudges } from "./sql/nudges.mjs";
9
9
  import { AnalysisResult, Analyzer, DatabaseDriver, DiscoveredColumnReference, JsonbOperator, Parser, SQLCommenterExtraction, SQLCommenterTag, SortContext, StatementType, TableReference, ignoredIdentifier } from "./sql/analyzer.mjs";
10
10
  import { CompactSelectListOptions, compactSelectList } from "./sql/display-query.mjs";
@@ -12,4 +12,4 @@ import { isIndexProbablyDroppable, isIndexSupported } from "./sql/indexes.mjs";
12
12
  import { CombinedExport, ExportedQuery, QuerySource, combinedDumpSql, dumpQueriesSql } from "./optimizer/dump.mjs";
13
13
  import { PssRewriter } from "./optimizer/pss-rewriter.mjs";
14
14
  import { deriveSentryEnvironment } from "./sentry.mjs";
15
- export { AnalysisResult, Analyzer, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexIdentifier, IndexOptimizer, IndexOrder, IndexRecommendation, IndexedTable, JsonbOperator, Nudge, OptimizeResult, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QuerySource, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
15
+ export { AnalysisResult, Analyzer, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexIdentifier, IndexOptimizer, IndexOrder, IndexRecommendation, IndexToCreate, IndexedTable, JsonbOperator, Nudge, OptimizeResult, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QuerySource, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
@@ -38,6 +38,7 @@ var IndexOptimizer = class IndexOptimizer {
38
38
  explainPlan: baseExplain.Plan
39
39
  };
40
40
  }
41
+ this.statistics.setAdditionalIndexes(toCreate);
41
42
  const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
42
43
  if (beforeQuery) await beforeQuery(tx);
43
44
  for (const permutation of toCreate) {
@@ -45,6 +46,7 @@ var IndexOptimizer = class IndexOptimizer {
45
46
  await tx.exec(createIndex);
46
47
  }
47
48
  });
49
+ this.statistics.setAdditionalIndexes([]);
48
50
  const finalCost = Number(finalExplain.Plan["Total Cost"]);
49
51
  if (this.config.debug) console.dir(finalExplain, { depth: null });
50
52
  const deltaPercentage = (baseCost - finalCost) / baseCost * 100;
@@ -1 +1 @@
1
- {"version":3,"file":"genalgo.cjs","names":["PostgresQueryBuilder","PgIdentifier","permutationsWithDescendingLength","green","yellow","magenta","blue","isIndexProbablyDroppable","dropIndex"],"sources":["../../src/optimizer/genalgo.ts"],"sourcesContent":["import type { NullTestType } from \"@pgsql/types\";\nimport { blue, gray, green, magenta, red, yellow } from \"colorette\";\nimport type { JsonbOperator } from \"../sql/analyzer.js\";\nimport type { SortContext } from \"../sql/analyzer.js\";\nimport { PostgresQueryBuilder } from \"../sql/builder.js\";\nimport {\n dropIndex,\n type Postgres,\n type PostgresExplainResult,\n type PostgresExplainStage,\n type PostgresTransaction,\n} from \"../sql/database.js\";\nimport { isIndexProbablyDroppable } from \"../sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"../sql/permutations.js\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexedTable, Statistics } from \"./statistics.js\";\n\nexport type IndexIdentifier = string;\n\nexport type IndexRecommendation = PermutedIndexCandidate & {\n definition: IndexIdentifier;\n};\n\ntype Color = (a: string) => string;\n\ntype IndexToCreate = PermutedIndexCandidate & {\n name: PgIdentifier;\n definition: IndexIdentifier;\n};\n\nexport class IndexOptimizer {\n static prefix = \"__qd_\";\n\n constructor(\n private readonly db: Postgres,\n private readonly statistics: Statistics,\n private existingIndexes: IndexedTable[],\n private readonly config: {\n trace?: boolean;\n debug?: boolean;\n } = {},\n ) {}\n\n async run(\n builder: PostgresQueryBuilder,\n indexes: RootIndexCandidate[],\n beforeQuery?: (tx: PostgresTransaction) => Promise<void>,\n ): Promise<OptimizeResult> {\n const baseExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n });\n const baseCost: number = Number(baseExplain.Plan[\"Total Cost\"]);\n if (baseCost === 0) {\n return {\n kind: \"zero_cost_plan\",\n explainPlan: baseExplain.Plan,\n };\n }\n const toCreate = this.indexesToCreate(indexes);\n if (toCreate.length === 0) {\n // No indexes to try: the 2nd EXPLAIN would be identical to the 1st,\n // so skip it. On large tables this saves a full re-run of the query.\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n return {\n kind: \"ok\",\n baseCost,\n finalCost: baseCost,\n newIndexes: new Set<string>(),\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes: new Map(),\n baseExplainPlan: baseExplain.Plan,\n explainPlan: baseExplain.Plan,\n };\n }\n const finalExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n\n // Then create recommended indexes\n for (const permutation of toCreate) {\n const createIndex = PostgresQueryBuilder.createIndex(\n permutation.definition,\n permutation.name,\n )\n .introspect()\n .build();\n\n await tx.exec(createIndex);\n }\n });\n const finalCost = Number(finalExplain.Plan[\"Total Cost\"]);\n if (this.config.debug) {\n console.dir(finalExplain, { depth: null });\n }\n const deltaPercentage = ((baseCost - finalCost) / baseCost) * 100;\n if (finalCost < baseCost) {\n console.log(\n ` 🎉🎉🎉 ${green(`+${deltaPercentage.toFixed(2).padStart(5, \"0\")}%`)}`,\n );\n } else if (finalCost > baseCost) {\n console.log(\n `${red(\n `-${Math.abs(deltaPercentage).toFixed(2).padStart(5, \"0\")}%`,\n )} ${gray(\"If there's a better index, we haven't tried it\")}`,\n );\n }\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n const finalIndexes = this.findUsedIndexes(finalExplain.Plan);\n const triedIndexes = new Map(\n toCreate.map((index) => [index.name.toString(), index]),\n );\n this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);\n\n return {\n kind: \"ok\",\n baseCost,\n finalCost,\n newIndexes: finalIndexes.newIndexes,\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes,\n baseExplainPlan: baseExplain.Plan,\n explainPlan: finalExplain.Plan,\n };\n }\n\n async runWithoutIndexes(builder: PostgresQueryBuilder) {\n return await this.testQueryWithStats(builder, async (tx) => {\n await this.dropExistingIndexes(tx);\n });\n }\n\n /**\n * Given the current indexes in the optimizer, transform them in some\n * way to change which indexes will be assumed to exist when optimizing\n *\n * @example\n * ```\n * // resets indexes\n * optimizer.transformIndexes(() => [])\n *\n * // adds new index\n * optimizer.transformIndexes(indexes => [...indexes, newIndex])\n * ```\n */\n transformIndexes(f: (indexes: IndexedTable[]) => IndexedTable[]) {\n const newIndexes = f(this.existingIndexes);\n this.existingIndexes = newIndexes;\n return this;\n }\n\n /**\n * Postgres has a limit of 63 characters for index names.\n * So we use this to make sure we don't derive it from a list of columns that can\n * overflow that limit.\n */\n private indexName(): PgIdentifier {\n const indexName =\n IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);\n return PgIdentifier.fromString(indexName);\n }\n\n // TODO: this doesn't belong in the optimizer\n private indexAlreadyExists(\n table: string,\n columns: RootIndexCandidate[],\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"btree\" &&\n index.table_name === table &&\n index.index_columns.length === columns.length &&\n index.index_columns.every((c, i) => {\n if (columns[i].column !== c.name) {\n return false;\n }\n\n // we should assume any index with `WHERE`\n // can't be counted as a duplicate\n if (columns[i].where) {\n return false;\n }\n\n if (columns[i].sort) {\n switch (columns[i].sort.dir) {\n // Sorting is ASC by default in postgres\n case \"SORTBY_DEFAULT\":\n case \"SORTBY_ASC\":\n if (c.order !== \"ASC\") {\n return false;\n }\n break;\n case \"SORTBY_DESC\":\n if (c.order !== \"DESC\") {\n return false;\n }\n break;\n }\n }\n return true;\n }),\n );\n }\n\n /**\n * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]\n **/\n private indexesToCreate(\n rootCandidates: RootIndexCandidate[],\n ): IndexToCreate[] {\n const expressionCandidates = rootCandidates.filter(\n (c) => c.jsonbExtraction,\n );\n const btreeCandidates = rootCandidates.filter(\n (c) => !c.jsonbOperator && !c.jsonbExtraction,\n );\n const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);\n\n const nextStage: IndexToCreate[] = [];\n\n const permutedIndexes =\n this.groupPotentialIndexColumnsByTable(btreeCandidates);\n for (const permutation of permutedIndexes.values()) {\n const { table: rawTable, schema: rawSchema, columns } = permutation;\n const permutations = permutationsWithDescendingLength(columns);\n for (const columns of permutations) {\n // TODO: accept PgIdentifier values instead\n // required refactoring `PermutedIndexCandidate`\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n const existingIndex = this.indexAlreadyExists(\n table.toString(),\n columns,\n );\n if (existingIndex) {\n continue;\n }\n const indexName = this.indexName();\n\n const definition = this.toDefinition({ table, schema, columns }).raw;\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns,\n definition,\n });\n }\n }\n\n const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);\n for (const group of ginGroups.values()) {\n const { schema: rawSchema, table: rawTable, column, operators } = group;\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n\n // jsonb_path_ops is smaller/faster but only supports @>.\n // All other operators (key-existence and jsonpath) need the full jsonb_ops.\n const needsFullOps = operators.some((op) => op !== \"@>\");\n const opclass = needsFullOps ? undefined : \"jsonb_path_ops\";\n\n const existingGin = this.ginIndexAlreadyExists(table.toString(), column);\n if (existingGin) {\n continue;\n }\n\n const indexName = this.indexName();\n const candidate: RootIndexCandidate = {\n schema: rawSchema,\n table: rawTable,\n column,\n };\n const definition = this.toGinDefinition({\n table,\n schema,\n column: PgIdentifier.fromString(column),\n opclass,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n indexMethod: \"gin\",\n opclass,\n });\n }\n\n // Expression B-tree indexes for JSONB path extraction (-> / ->>)\n const seenExpressions = new Set<string>();\n for (const candidate of expressionCandidates) {\n const expression = candidate.jsonbExtraction!;\n const key = `${candidate.schema}.${candidate.table}.${expression}`;\n if (seenExpressions.has(key)) continue;\n seenExpressions.add(key);\n\n const schema = PgIdentifier.fromString(candidate.schema);\n const table = PgIdentifier.fromString(candidate.table);\n const indexName = this.indexName();\n const definition = this.toExpressionDefinition({\n table,\n schema,\n expression,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n });\n }\n\n return nextStage;\n }\n\n private toDefinition({\n schema,\n table,\n columns,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n columns: RootIndexCandidate[];\n }) {\n const make = (col: Color, order: Color, _where: Color, _keyword: Color) => {\n // TODO: refactor all of this class to accept PgIdentifiers\n let fullyQualifiedTable: PgIdentifier;\n\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const baseColumn = `${fullyQualifiedTable}(${columns\n .map((c) => {\n const column = PgIdentifier.fromString(c.column);\n const direction = c.sort && this.sortDirection(c.sort);\n const nulls = c.sort && this.nullsOrder(c.sort);\n let sort = col(column.toString());\n if (direction) {\n sort += ` ${order(direction)}`;\n }\n if (nulls) {\n sort += ` ${order(nulls)}`;\n }\n return sort;\n })\n .join(\", \")})`;\n // TODO: add support for generating partial indexes\n // if (clauses.length > 0) {\n // return `${baseColumn} ${where(\"where\")} ${clauses.join(\" and \")}`;\n // }\n return baseColumn;\n };\n const id: Color = (a) => a;\n const raw = make(id, id, id, id);\n const colored = make(green, yellow, magenta, blue);\n return { raw, colored };\n }\n\n private toGinDefinition({\n schema,\n table,\n column,\n opclass,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n column: PgIdentifier;\n opclass?: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const opclassSuffix = opclass ? ` ${opclass}` : \"\";\n return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;\n }\n\n private toExpressionDefinition({\n schema,\n table,\n expression,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n expression: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n return `${fullyQualifiedTable}((${expression}))`;\n }\n\n private groupGinCandidatesByColumn(candidates: RootIndexCandidate[]) {\n const groups = new Map<\n string,\n {\n schema: string;\n table: string;\n column: string;\n operators: JsonbOperator[];\n }\n >();\n for (const c of candidates) {\n if (!c.jsonbOperator) continue;\n const key = `${c.schema}.${c.table}.${c.column}`;\n const existing = groups.get(key);\n if (existing) {\n if (!existing.operators.includes(c.jsonbOperator)) {\n existing.operators.push(c.jsonbOperator);\n }\n } else {\n groups.set(key, {\n schema: c.schema,\n table: c.table,\n column: c.column,\n operators: [c.jsonbOperator],\n });\n }\n }\n return groups;\n }\n\n private ginIndexAlreadyExists(\n table: string,\n column: string,\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"gin\" &&\n index.table_name === table &&\n index.index_columns.some((c) => c.name === column),\n );\n }\n\n /**\n * Drop indexes that can be dropped. Ignore the ones that can't\n */\n private async dropExistingIndexes(tx: PostgresTransaction) {\n for (const index of this.existingIndexes) {\n if (!isIndexProbablyDroppable(index)) {\n continue;\n }\n const indexName = PgIdentifier.fromParts(\n index.schema_name,\n index.index_name,\n );\n await dropIndex(tx, indexName);\n }\n }\n\n private whereClause(c: RootIndexCandidate, col: Color, keyword: Color) {\n if (!c.where) {\n return \"\";\n }\n if (c.where.nulltest === \"IS_NULL\") {\n return `${col(`\"${c.column}\"`)} is ${keyword(\"null\")}`;\n }\n if (c.where.nulltest === \"IS_NOT_NULL\") {\n return `${col(`\"${c.column}\"`)} is not ${keyword(\"null\")}`;\n }\n return \"\";\n }\n\n private nullsOrder(s: SortContext) {\n if (!s.nulls) {\n return \"\";\n }\n switch (s.nulls) {\n case \"SORTBY_NULLS_FIRST\":\n return \"nulls first\";\n case \"SORTBY_NULLS_LAST\":\n return \"nulls last\";\n case \"SORTBY_NULLS_DEFAULT\":\n default:\n return \"\";\n }\n }\n\n private sortDirection(s: SortContext) {\n if (!s.dir) {\n return \"\";\n }\n switch (s.dir) {\n case \"SORTBY_DESC\":\n return \"desc\";\n case \"SORTBY_ASC\":\n return \"asc\";\n case \"SORTBY_DEFAULT\":\n // god help us if we ever run into this\n case \"SORTBY_USING\":\n default:\n return \"\";\n }\n }\n\n async testQueryWithStats(\n builder: PostgresQueryBuilder,\n f?: (tx: PostgresTransaction) => Promise<void>,\n options?: { params?: unknown[]; genericPlan?: boolean },\n ): Promise<{ Plan: PostgresExplainStage }> {\n try {\n await this.db.transaction(async (tx) => {\n await f?.(tx);\n await this.statistics.restoreStats(tx);\n const flags = [\"format json\"];\n if (options && !options.genericPlan) {\n flags.push(\"analyze\");\n if (this.config.trace) {\n // trace can only be used alongside analyze\n // since it depends on the results of the query execution\n flags.push(\"trace\");\n }\n } else {\n flags.push(\"generic_plan\");\n }\n const { commands, query } = builder\n .introspect()\n .explain(flags)\n .buildParts();\n // this is done in a separate step to prevent sending multiple commands when using parameters\n await tx.exec(commands);\n const result = await tx.exec<PostgresExplainResult>(\n query,\n options?.params,\n );\n const explain = result[0][\"QUERY PLAN\"][0];\n throw new RollbackError(explain);\n });\n } catch (error) {\n if (error instanceof RollbackError) {\n return error.value;\n }\n throw error;\n }\n throw new Error(\"Unreachable\");\n }\n\n private groupPotentialIndexColumnsByTable(indexes: RootIndexCandidate[]) {\n const tableColumns: Map<\n string,\n { schema: string; table: string; columns: RootIndexCandidate[] }\n > = new Map();\n for (const index of indexes) {\n const existing = tableColumns.get(`${index.schema}.${index.table}`);\n if (existing) {\n existing.columns.push(index);\n } else {\n tableColumns.set(`${index.schema}.${index.table}`, {\n table: index.table,\n schema: index.schema,\n columns: [index],\n });\n }\n }\n return tableColumns;\n }\n\n private findUsedIndexes(explain: Record<string, any>) {\n const newIndexes: Set<string> = new Set();\n const existingIndexes: Set<string> = new Set();\n const prefix = IndexOptimizer.prefix;\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (indexName) {\n // Check for prefix at start or embedded (for hypertable chunk indexes like _hyper_1_1_chunk___qd_xxx)\n if (indexName.startsWith(prefix)) {\n newIndexes.add(indexName);\n } else if (indexName.includes(prefix)) {\n // Extract the actual index name from chunk-prefixed names (e.g., _hyper_1_1_chunk___qd_xxx -> __qd_xxx)\n const actualName = indexName.substring(indexName.indexOf(prefix));\n newIndexes.add(actualName);\n } else {\n existingIndexes.add(indexName);\n }\n }\n });\n return {\n newIndexes,\n existingIndexes,\n };\n }\n\n private replaceUsedIndexesWithDefinition(\n explain: Record<string, any>,\n triedIndexes: Map<string, IndexRecommendation>,\n ) {\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (typeof indexName === \"string\") {\n const recommendation = triedIndexes.get(indexName);\n if (recommendation) {\n stage[\"Index Name\"] = recommendation.definition;\n }\n }\n });\n }\n}\n\nfunction walkExplain(explain: Record<string, any>, f: (stage: any) => void) {\n function go(plan: any) {\n f(plan);\n if (plan.Plans) {\n for (const p of plan.Plans) {\n go(p);\n }\n }\n }\n go(explain);\n}\n\nexport type OptimizeResult =\n | {\n kind: \"ok\";\n baseExplainPlan: PostgresExplainStage;\n baseCost: number;\n finalCost: number;\n newIndexes: Set<string>;\n existingIndexes: Set<string>;\n triedIndexes: Map<string, IndexRecommendation>;\n explainPlan: PostgresExplainStage;\n }\n | {\n kind: \"zero_cost_plan\";\n explainPlan: PostgresExplainStage;\n };\n\nclass RollbackError<T> {\n constructor(public readonly value?: T) {}\n}\n\nexport type RootIndexCandidate = {\n schema: string;\n table: string;\n column: string;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n};\n\nexport type PermutedIndexCandidate = {\n schema: string;\n table: string;\n columns: RootIndexCandidate[];\n // TODO: functional indexes\n where?: string;\n indexMethod?: \"btree\" | \"gin\";\n opclass?: string;\n};\n\nexport const PROCEED = Symbol(\"PROCEED\");\nexport const SKIP = Symbol(\"SKIP\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"genalgo.cjs","names":["PostgresQueryBuilder","PgIdentifier","permutationsWithDescendingLength","green","yellow","magenta","blue","isIndexProbablyDroppable","dropIndex"],"sources":["../../src/optimizer/genalgo.ts"],"sourcesContent":["import type { NullTestType } from \"@pgsql/types\";\nimport { blue, gray, green, magenta, red, yellow } from \"colorette\";\nimport type { JsonbOperator } from \"../sql/analyzer.js\";\nimport type { SortContext } from \"../sql/analyzer.js\";\nimport { PostgresQueryBuilder } from \"../sql/builder.js\";\nimport {\n dropIndex,\n type Postgres,\n type PostgresExplainResult,\n type PostgresExplainStage,\n type PostgresTransaction,\n} from \"../sql/database.js\";\nimport { isIndexProbablyDroppable } from \"../sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"../sql/permutations.js\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexedTable, Statistics } from \"./statistics.js\";\n\nexport type IndexIdentifier = string;\n\nexport type IndexRecommendation = PermutedIndexCandidate & {\n definition: IndexIdentifier;\n};\n\ntype Color = (a: string) => string;\n\nexport type IndexToCreate = PermutedIndexCandidate & {\n name: PgIdentifier;\n definition: IndexIdentifier;\n};\n\nexport class IndexOptimizer {\n static prefix = \"__qd_\";\n\n constructor(\n private readonly db: Postgres,\n private readonly statistics: Statistics,\n private existingIndexes: IndexedTable[],\n private readonly config: {\n trace?: boolean;\n debug?: boolean;\n } = {},\n ) {}\n\n async run(\n builder: PostgresQueryBuilder,\n indexes: RootIndexCandidate[],\n beforeQuery?: (tx: PostgresTransaction) => Promise<void>,\n ): Promise<OptimizeResult> {\n const baseExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n });\n const baseCost: number = Number(baseExplain.Plan[\"Total Cost\"]);\n if (baseCost === 0) {\n return {\n kind: \"zero_cost_plan\",\n explainPlan: baseExplain.Plan,\n };\n }\n const toCreate = this.indexesToCreate(indexes);\n if (toCreate.length === 0) {\n // No indexes to try: the 2nd EXPLAIN would be identical to the 1st,\n // so skip it. On large tables this saves a full re-run of the query.\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n return {\n kind: \"ok\",\n baseCost,\n finalCost: baseCost,\n newIndexes: new Set<string>(),\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes: new Map(),\n baseExplainPlan: baseExplain.Plan,\n explainPlan: baseExplain.Plan,\n };\n }\n this.statistics.setAdditionalIndexes(toCreate);\n const finalExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n\n // Then create recommended indexes\n for (const permutation of toCreate) {\n const createIndex = PostgresQueryBuilder.createIndex(\n permutation.definition,\n permutation.name,\n )\n .introspect()\n .build();\n\n await tx.exec(createIndex);\n }\n });\n this.statistics.setAdditionalIndexes([]);\n const finalCost = Number(finalExplain.Plan[\"Total Cost\"]);\n if (this.config.debug) {\n console.dir(finalExplain, { depth: null });\n }\n const deltaPercentage = ((baseCost - finalCost) / baseCost) * 100;\n if (finalCost < baseCost) {\n console.log(\n ` 🎉🎉🎉 ${green(`+${deltaPercentage.toFixed(2).padStart(5, \"0\")}%`)}`,\n );\n } else if (finalCost > baseCost) {\n console.log(\n `${red(\n `-${Math.abs(deltaPercentage).toFixed(2).padStart(5, \"0\")}%`,\n )} ${gray(\"If there's a better index, we haven't tried it\")}`,\n );\n }\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n const finalIndexes = this.findUsedIndexes(finalExplain.Plan);\n const triedIndexes = new Map(\n toCreate.map((index) => [index.name.toString(), index]),\n );\n this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);\n\n return {\n kind: \"ok\",\n baseCost,\n finalCost,\n newIndexes: finalIndexes.newIndexes,\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes,\n baseExplainPlan: baseExplain.Plan,\n explainPlan: finalExplain.Plan,\n };\n }\n\n async runWithoutIndexes(builder: PostgresQueryBuilder) {\n return await this.testQueryWithStats(builder, async (tx) => {\n await this.dropExistingIndexes(tx);\n });\n }\n\n /**\n * Given the current indexes in the optimizer, transform them in some\n * way to change which indexes will be assumed to exist when optimizing\n *\n * @example\n * ```\n * // resets indexes\n * optimizer.transformIndexes(() => [])\n *\n * // adds new index\n * optimizer.transformIndexes(indexes => [...indexes, newIndex])\n * ```\n */\n transformIndexes(f: (indexes: IndexedTable[]) => IndexedTable[]) {\n const newIndexes = f(this.existingIndexes);\n this.existingIndexes = newIndexes;\n return this;\n }\n\n /**\n * Postgres has a limit of 63 characters for index names.\n * So we use this to make sure we don't derive it from a list of columns that can\n * overflow that limit.\n */\n private indexName(): PgIdentifier {\n const indexName =\n IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);\n return PgIdentifier.fromString(indexName);\n }\n\n // TODO: this doesn't belong in the optimizer\n private indexAlreadyExists(\n table: string,\n columns: RootIndexCandidate[],\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"btree\" &&\n index.table_name === table &&\n index.index_columns.length === columns.length &&\n index.index_columns.every((c, i) => {\n if (columns[i].column !== c.name) {\n return false;\n }\n\n // we should assume any index with `WHERE`\n // can't be counted as a duplicate\n if (columns[i].where) {\n return false;\n }\n\n if (columns[i].sort) {\n switch (columns[i].sort.dir) {\n // Sorting is ASC by default in postgres\n case \"SORTBY_DEFAULT\":\n case \"SORTBY_ASC\":\n if (c.order !== \"ASC\") {\n return false;\n }\n break;\n case \"SORTBY_DESC\":\n if (c.order !== \"DESC\") {\n return false;\n }\n break;\n }\n }\n return true;\n }),\n );\n }\n\n /**\n * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]\n **/\n private indexesToCreate(\n rootCandidates: RootIndexCandidate[],\n ): IndexToCreate[] {\n const expressionCandidates = rootCandidates.filter(\n (c) => c.jsonbExtraction,\n );\n const btreeCandidates = rootCandidates.filter(\n (c) => !c.jsonbOperator && !c.jsonbExtraction,\n );\n const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);\n\n const nextStage: IndexToCreate[] = [];\n\n const permutedIndexes =\n this.groupPotentialIndexColumnsByTable(btreeCandidates);\n for (const permutation of permutedIndexes.values()) {\n const { table: rawTable, schema: rawSchema, columns } = permutation;\n const permutations = permutationsWithDescendingLength(columns);\n for (const columns of permutations) {\n // TODO: accept PgIdentifier values instead\n // required refactoring `PermutedIndexCandidate`\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n const existingIndex = this.indexAlreadyExists(\n table.toString(),\n columns,\n );\n if (existingIndex) {\n continue;\n }\n const indexName = this.indexName();\n\n const definition = this.toDefinition({ table, schema, columns }).raw;\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns,\n definition,\n });\n }\n }\n\n const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);\n for (const group of ginGroups.values()) {\n const { schema: rawSchema, table: rawTable, column, operators } = group;\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n\n // jsonb_path_ops is smaller/faster but only supports @>.\n // All other operators (key-existence and jsonpath) need the full jsonb_ops.\n const needsFullOps = operators.some((op) => op !== \"@>\");\n const opclass = needsFullOps ? undefined : \"jsonb_path_ops\";\n\n const existingGin = this.ginIndexAlreadyExists(table.toString(), column);\n if (existingGin) {\n continue;\n }\n\n const indexName = this.indexName();\n const candidate: RootIndexCandidate = {\n schema: rawSchema,\n table: rawTable,\n column,\n };\n const definition = this.toGinDefinition({\n table,\n schema,\n column: PgIdentifier.fromString(column),\n opclass,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n indexMethod: \"gin\",\n opclass,\n });\n }\n\n // Expression B-tree indexes for JSONB path extraction (-> / ->>)\n const seenExpressions = new Set<string>();\n for (const candidate of expressionCandidates) {\n const expression = candidate.jsonbExtraction!;\n const key = `${candidate.schema}.${candidate.table}.${expression}`;\n if (seenExpressions.has(key)) continue;\n seenExpressions.add(key);\n\n const schema = PgIdentifier.fromString(candidate.schema);\n const table = PgIdentifier.fromString(candidate.table);\n const indexName = this.indexName();\n const definition = this.toExpressionDefinition({\n table,\n schema,\n expression,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n });\n }\n\n return nextStage;\n }\n\n private toDefinition({\n schema,\n table,\n columns,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n columns: RootIndexCandidate[];\n }) {\n const make = (col: Color, order: Color, _where: Color, _keyword: Color) => {\n // TODO: refactor all of this class to accept PgIdentifiers\n let fullyQualifiedTable: PgIdentifier;\n\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const baseColumn = `${fullyQualifiedTable}(${columns\n .map((c) => {\n const column = PgIdentifier.fromString(c.column);\n const direction = c.sort && this.sortDirection(c.sort);\n const nulls = c.sort && this.nullsOrder(c.sort);\n let sort = col(column.toString());\n if (direction) {\n sort += ` ${order(direction)}`;\n }\n if (nulls) {\n sort += ` ${order(nulls)}`;\n }\n return sort;\n })\n .join(\", \")})`;\n // TODO: add support for generating partial indexes\n // if (clauses.length > 0) {\n // return `${baseColumn} ${where(\"where\")} ${clauses.join(\" and \")}`;\n // }\n return baseColumn;\n };\n const id: Color = (a) => a;\n const raw = make(id, id, id, id);\n const colored = make(green, yellow, magenta, blue);\n return { raw, colored };\n }\n\n private toGinDefinition({\n schema,\n table,\n column,\n opclass,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n column: PgIdentifier;\n opclass?: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const opclassSuffix = opclass ? ` ${opclass}` : \"\";\n return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;\n }\n\n private toExpressionDefinition({\n schema,\n table,\n expression,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n expression: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n return `${fullyQualifiedTable}((${expression}))`;\n }\n\n private groupGinCandidatesByColumn(candidates: RootIndexCandidate[]) {\n const groups = new Map<\n string,\n {\n schema: string;\n table: string;\n column: string;\n operators: JsonbOperator[];\n }\n >();\n for (const c of candidates) {\n if (!c.jsonbOperator) continue;\n const key = `${c.schema}.${c.table}.${c.column}`;\n const existing = groups.get(key);\n if (existing) {\n if (!existing.operators.includes(c.jsonbOperator)) {\n existing.operators.push(c.jsonbOperator);\n }\n } else {\n groups.set(key, {\n schema: c.schema,\n table: c.table,\n column: c.column,\n operators: [c.jsonbOperator],\n });\n }\n }\n return groups;\n }\n\n private ginIndexAlreadyExists(\n table: string,\n column: string,\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"gin\" &&\n index.table_name === table &&\n index.index_columns.some((c) => c.name === column),\n );\n }\n\n /**\n * Drop indexes that can be dropped. Ignore the ones that can't\n */\n private async dropExistingIndexes(tx: PostgresTransaction) {\n for (const index of this.existingIndexes) {\n if (!isIndexProbablyDroppable(index)) {\n continue;\n }\n const indexName = PgIdentifier.fromParts(\n index.schema_name,\n index.index_name,\n );\n await dropIndex(tx, indexName);\n }\n }\n\n private whereClause(c: RootIndexCandidate, col: Color, keyword: Color) {\n if (!c.where) {\n return \"\";\n }\n if (c.where.nulltest === \"IS_NULL\") {\n return `${col(`\"${c.column}\"`)} is ${keyword(\"null\")}`;\n }\n if (c.where.nulltest === \"IS_NOT_NULL\") {\n return `${col(`\"${c.column}\"`)} is not ${keyword(\"null\")}`;\n }\n return \"\";\n }\n\n private nullsOrder(s: SortContext) {\n if (!s.nulls) {\n return \"\";\n }\n switch (s.nulls) {\n case \"SORTBY_NULLS_FIRST\":\n return \"nulls first\";\n case \"SORTBY_NULLS_LAST\":\n return \"nulls last\";\n case \"SORTBY_NULLS_DEFAULT\":\n default:\n return \"\";\n }\n }\n\n private sortDirection(s: SortContext) {\n if (!s.dir) {\n return \"\";\n }\n switch (s.dir) {\n case \"SORTBY_DESC\":\n return \"desc\";\n case \"SORTBY_ASC\":\n return \"asc\";\n case \"SORTBY_DEFAULT\":\n // god help us if we ever run into this\n case \"SORTBY_USING\":\n default:\n return \"\";\n }\n }\n\n async testQueryWithStats(\n builder: PostgresQueryBuilder,\n f?: (tx: PostgresTransaction) => Promise<void>,\n options?: { params?: unknown[]; genericPlan?: boolean },\n ): Promise<{ Plan: PostgresExplainStage }> {\n try {\n await this.db.transaction(async (tx) => {\n await f?.(tx);\n await this.statistics.restoreStats(tx);\n const flags = [\"format json\"];\n if (options && !options.genericPlan) {\n flags.push(\"analyze\");\n if (this.config.trace) {\n // trace can only be used alongside analyze\n // since it depends on the results of the query execution\n flags.push(\"trace\");\n }\n } else {\n flags.push(\"generic_plan\");\n }\n const { commands, query } = builder\n .introspect()\n .explain(flags)\n .buildParts();\n // this is done in a separate step to prevent sending multiple commands when using parameters\n await tx.exec(commands);\n const result = await tx.exec<PostgresExplainResult>(\n query,\n options?.params,\n );\n const explain = result[0][\"QUERY PLAN\"][0];\n throw new RollbackError(explain);\n });\n } catch (error) {\n if (error instanceof RollbackError) {\n return error.value;\n }\n throw error;\n }\n throw new Error(\"Unreachable\");\n }\n\n private groupPotentialIndexColumnsByTable(indexes: RootIndexCandidate[]) {\n const tableColumns: Map<\n string,\n { schema: string; table: string; columns: RootIndexCandidate[] }\n > = new Map();\n for (const index of indexes) {\n const existing = tableColumns.get(`${index.schema}.${index.table}`);\n if (existing) {\n existing.columns.push(index);\n } else {\n tableColumns.set(`${index.schema}.${index.table}`, {\n table: index.table,\n schema: index.schema,\n columns: [index],\n });\n }\n }\n return tableColumns;\n }\n\n private findUsedIndexes(explain: Record<string, any>) {\n const newIndexes: Set<string> = new Set();\n const existingIndexes: Set<string> = new Set();\n const prefix = IndexOptimizer.prefix;\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (indexName) {\n // Check for prefix at start or embedded (for hypertable chunk indexes like _hyper_1_1_chunk___qd_xxx)\n if (indexName.startsWith(prefix)) {\n newIndexes.add(indexName);\n } else if (indexName.includes(prefix)) {\n // Extract the actual index name from chunk-prefixed names (e.g., _hyper_1_1_chunk___qd_xxx -> __qd_xxx)\n const actualName = indexName.substring(indexName.indexOf(prefix));\n newIndexes.add(actualName);\n } else {\n existingIndexes.add(indexName);\n }\n }\n });\n return {\n newIndexes,\n existingIndexes,\n };\n }\n\n private replaceUsedIndexesWithDefinition(\n explain: Record<string, any>,\n triedIndexes: Map<string, IndexRecommendation>,\n ) {\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (typeof indexName === \"string\") {\n const recommendation = triedIndexes.get(indexName);\n if (recommendation) {\n stage[\"Index Name\"] = recommendation.definition;\n }\n }\n });\n }\n}\n\nfunction walkExplain(explain: Record<string, any>, f: (stage: any) => void) {\n function go(plan: any) {\n f(plan);\n if (plan.Plans) {\n for (const p of plan.Plans) {\n go(p);\n }\n }\n }\n go(explain);\n}\n\nexport type OptimizeResult =\n | {\n kind: \"ok\";\n baseExplainPlan: PostgresExplainStage;\n baseCost: number;\n finalCost: number;\n newIndexes: Set<string>;\n existingIndexes: Set<string>;\n triedIndexes: Map<string, IndexRecommendation>;\n explainPlan: PostgresExplainStage;\n }\n | {\n kind: \"zero_cost_plan\";\n explainPlan: PostgresExplainStage;\n };\n\nclass RollbackError<T> {\n constructor(public readonly value?: T) {}\n}\n\nexport type RootIndexCandidate = {\n schema: string;\n table: string;\n column: string;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n};\n\nexport type PermutedIndexCandidate = {\n schema: string;\n table: string;\n columns: RootIndexCandidate[];\n // TODO: functional indexes\n where?: string;\n indexMethod?: \"btree\" | \"gin\";\n opclass?: string;\n};\n\nexport const PROCEED = Symbol(\"PROCEED\");\nexport const SKIP = Symbol(\"SKIP\");\n"],"mappings":";;;;;;;;;;AA8BA,IAAa,iBAAb,MAAa,eAAe;CAG1B,YACE,IACA,YACA,iBACA,SAGI,EAAE,EACN;AAPiB,OAAA,KAAA;AACA,OAAA,aAAA;AACT,OAAA,kBAAA;AACS,OAAA,SAAA;;CAMnB,MAAM,IACJ,SACA,SACA,aACyB;EACzB,MAAM,cAAc,MAAM,KAAK,mBAAmB,SAAS,OAAO,OAAO;AACvE,OAAI,YACF,OAAM,YAAY,GAAG;IAEvB;EACF,MAAM,WAAmB,OAAO,YAAY,KAAK,cAAc;AAC/D,MAAI,aAAa,EACf,QAAO;GACL,MAAM;GACN,aAAa,YAAY;GAC1B;EAEH,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAC9C,MAAI,SAAS,WAAW,GAAG;GAGzB,MAAM,cAAc,KAAK,gBAAgB,YAAY,KAAK;AAC1D,UAAO;IACL,MAAM;IACN;IACA,WAAW;IACX,4BAAY,IAAI,KAAa;IAC7B,iBAAiB,YAAY;IAC7B,8BAAc,IAAI,KAAK;IACvB,iBAAiB,YAAY;IAC7B,aAAa,YAAY;IAC1B;;AAEH,OAAK,WAAW,qBAAqB,SAAS;EAC9C,MAAM,eAAe,MAAM,KAAK,mBAAmB,SAAS,OAAO,OAAO;AACxE,OAAI,YACF,OAAM,YAAY,GAAG;AAIvB,QAAK,MAAM,eAAe,UAAU;IAClC,MAAM,cAAcA,gBAAAA,qBAAqB,YACvC,YAAY,YACZ,YAAY,KACb,CACE,YAAY,CACZ,OAAO;AAEV,UAAM,GAAG,KAAK,YAAY;;IAE5B;AACF,OAAK,WAAW,qBAAqB,EAAE,CAAC;EACxC,MAAM,YAAY,OAAO,aAAa,KAAK,cAAc;AACzD,MAAI,KAAK,OAAO,MACd,SAAQ,IAAI,cAAc,EAAE,OAAO,MAAM,CAAC;EAE5C,MAAM,mBAAoB,WAAW,aAAa,WAAY;AAC9D,MAAI,YAAY,SACd,SAAQ,IACN,YAAA,GAAA,UAAA,OAAiB,IAAI,gBAAgB,QAAQ,EAAE,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,GACrE;WACQ,YAAY,SACrB,SAAQ,IACN,IAAA,GAAA,UAAA,KACE,IAAI,KAAK,IAAI,gBAAgB,CAAC,QAAQ,EAAE,CAAC,SAAS,GAAG,IAAI,CAAC,GAC3D,CAAC,IAAA,GAAA,UAAA,MAAQ,iDAAiD,GAC5D;EAEH,MAAM,cAAc,KAAK,gBAAgB,YAAY,KAAK;EAC1D,MAAM,eAAe,KAAK,gBAAgB,aAAa,KAAK;EAC5D,MAAM,eAAe,IAAI,IACvB,SAAS,KAAK,UAAU,CAAC,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC,CACxD;AACD,OAAK,iCAAiC,aAAa,MAAM,aAAa;AAEtE,SAAO;GACL,MAAM;GACN;GACA;GACA,YAAY,aAAa;GACzB,iBAAiB,YAAY;GAC7B;GACA,iBAAiB,YAAY;GAC7B,aAAa,aAAa;GAC3B;;CAGH,MAAM,kBAAkB,SAA+B;AACrD,SAAO,MAAM,KAAK,mBAAmB,SAAS,OAAO,OAAO;AAC1D,SAAM,KAAK,oBAAoB,GAAG;IAClC;;;;;;;;;;;;;;;CAgBJ,iBAAiB,GAAgD;EAC/D,MAAM,aAAa,EAAE,KAAK,gBAAgB;AAC1C,OAAK,kBAAkB;AACvB,SAAO;;;;;;;CAQT,YAAkC;EAChC,MAAM,YACJ,eAAe,SAAS,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,UAAU,GAAG,GAAG;AACrE,SAAOC,sBAAAA,aAAa,WAAW,UAAU;;CAI3C,mBACE,OACA,SAC0B;AAC1B,SAAO,KAAK,gBAAgB,MACzB,UACC,MAAM,eAAe,WACrB,MAAM,eAAe,SACrB,MAAM,cAAc,WAAW,QAAQ,UACvC,MAAM,cAAc,OAAO,GAAG,MAAM;AAClC,OAAI,QAAQ,GAAG,WAAW,EAAE,KAC1B,QAAO;AAKT,OAAI,QAAQ,GAAG,MACb,QAAO;AAGT,OAAI,QAAQ,GAAG,KACb,SAAQ,QAAQ,GAAG,KAAK,KAAxB;IAEE,KAAK;IACL,KAAK;AACH,SAAI,EAAE,UAAU,MACd,QAAO;AAET;IACF,KAAK;AACH,SAAI,EAAE,UAAU,OACd,QAAO;AAET;;AAGN,UAAO;IACP,CACL;;;;;CAMH,gBACE,gBACiB;EACjB,MAAM,uBAAuB,eAAe,QACzC,MAAM,EAAE,gBACV;EACD,MAAM,kBAAkB,eAAe,QACpC,MAAM,CAAC,EAAE,iBAAiB,CAAC,EAAE,gBAC/B;EACD,MAAM,gBAAgB,eAAe,QAAQ,MAAM,EAAE,cAAc;EAEnE,MAAM,YAA6B,EAAE;EAErC,MAAM,kBACJ,KAAK,kCAAkC,gBAAgB;AACzD,OAAK,MAAM,eAAe,gBAAgB,QAAQ,EAAE;GAClD,MAAM,EAAE,OAAO,UAAU,QAAQ,WAAW,YAAY;GACxD,MAAM,eAAeC,qBAAAA,iCAAiC,QAAQ;AAC9D,QAAK,MAAM,WAAW,cAAc;IAGlC,MAAM,SAASD,sBAAAA,aAAa,WAAW,UAAU;IACjD,MAAM,QAAQA,sBAAAA,aAAa,WAAW,SAAS;AAK/C,QAJsB,KAAK,mBACzB,MAAM,UAAU,EAChB,QACD,CAEC;IAEF,MAAM,YAAY,KAAK,WAAW;IAElC,MAAM,aAAa,KAAK,aAAa;KAAE;KAAO;KAAQ;KAAS,CAAC,CAAC;AAEjE,cAAU,KAAK;KACb,MAAM;KACN,QAAQ,OAAO,UAAU;KACzB,OAAO,MAAM,UAAU;KACvB;KACA;KACD,CAAC;;;EAIN,MAAM,YAAY,KAAK,2BAA2B,cAAc;AAChE,OAAK,MAAM,SAAS,UAAU,QAAQ,EAAE;GACtC,MAAM,EAAE,QAAQ,WAAW,OAAO,UAAU,QAAQ,cAAc;GAClE,MAAM,SAASA,sBAAAA,aAAa,WAAW,UAAU;GACjD,MAAM,QAAQA,sBAAAA,aAAa,WAAW,SAAS;GAK/C,MAAM,UADe,UAAU,MAAM,OAAO,OAAO,KAAK,GACzB,KAAA,IAAY;AAG3C,OADoB,KAAK,sBAAsB,MAAM,UAAU,EAAE,OAAO,CAEtE;GAGF,MAAM,YAAY,KAAK,WAAW;GAClC,MAAM,YAAgC;IACpC,QAAQ;IACR,OAAO;IACP;IACD;GACD,MAAM,aAAa,KAAK,gBAAgB;IACtC;IACA;IACA,QAAQA,sBAAAA,aAAa,WAAW,OAAO;IACvC;IACD,CAAC;AAEF,aAAU,KAAK;IACb,MAAM;IACN,QAAQ,OAAO,UAAU;IACzB,OAAO,MAAM,UAAU;IACvB,SAAS,CAAC,UAAU;IACpB;IACA,aAAa;IACb;IACD,CAAC;;EAIJ,MAAM,kCAAkB,IAAI,KAAa;AACzC,OAAK,MAAM,aAAa,sBAAsB;GAC5C,MAAM,aAAa,UAAU;GAC7B,MAAM,MAAM,GAAG,UAAU,OAAO,GAAG,UAAU,MAAM,GAAG;AACtD,OAAI,gBAAgB,IAAI,IAAI,CAAE;AAC9B,mBAAgB,IAAI,IAAI;GAExB,MAAM,SAASA,sBAAAA,aAAa,WAAW,UAAU,OAAO;GACxD,MAAM,QAAQA,sBAAAA,aAAa,WAAW,UAAU,MAAM;GACtD,MAAM,YAAY,KAAK,WAAW;GAClC,MAAM,aAAa,KAAK,uBAAuB;IAC7C;IACA;IACA;IACD,CAAC;AAEF,aAAU,KAAK;IACb,MAAM;IACN,QAAQ,OAAO,UAAU;IACzB,OAAO,MAAM,UAAU;IACvB,SAAS,CAAC,UAAU;IACpB;IACD,CAAC;;AAGJ,SAAO;;CAGT,aAAqB,EACnB,QACA,OACA,WAKC;EACD,MAAM,QAAQ,KAAY,OAAc,QAAe,aAAoB;GAEzE,IAAI;AAEJ,OAAI,OAAO,UAAU,KAAK,SACxB,uBAAsB;OAEtB,uBAAsBA,sBAAAA,aAAa,UAAU,QAAQ,MAAM;AAqB7D,UAnBmB,GAAG,oBAAoB,GAAG,QAC1C,KAAK,MAAM;IACV,MAAM,SAASA,sBAAAA,aAAa,WAAW,EAAE,OAAO;IAChD,MAAM,YAAY,EAAE,QAAQ,KAAK,cAAc,EAAE,KAAK;IACtD,MAAM,QAAQ,EAAE,QAAQ,KAAK,WAAW,EAAE,KAAK;IAC/C,IAAI,OAAO,IAAI,OAAO,UAAU,CAAC;AACjC,QAAI,UACF,SAAQ,IAAI,MAAM,UAAU;AAE9B,QAAI,MACF,SAAQ,IAAI,MAAM,MAAM;AAE1B,WAAO;KACP,CACD,KAAK,KAAK,CAAC;;EAOhB,MAAM,MAAa,MAAM;AAGzB,SAAO;GAAE,KAFG,KAAK,IAAI,IAAI,IAAI,GAAG;GAElB,SADE,KAAKE,UAAAA,OAAOC,UAAAA,QAAQC,UAAAA,SAASC,UAAAA,KAAK;GAC3B;;CAGzB,gBAAwB,EACtB,QACA,OACA,QACA,WAMS;EACT,IAAI;AACJ,MAAI,OAAO,UAAU,KAAK,SACxB,uBAAsB;MAEtB,uBAAsBL,sBAAAA,aAAa,UAAU,QAAQ,MAAM;EAE7D,MAAM,gBAAgB,UAAU,IAAI,YAAY;AAChD,SAAO,GAAG,oBAAoB,cAAc,SAAS,cAAc;;CAGrE,uBAA+B,EAC7B,QACA,OACA,cAKS;EACT,IAAI;AACJ,MAAI,OAAO,UAAU,KAAK,SACxB,uBAAsB;MAEtB,uBAAsBA,sBAAAA,aAAa,UAAU,QAAQ,MAAM;AAE7D,SAAO,GAAG,oBAAoB,IAAI,WAAW;;CAG/C,2BAAmC,YAAkC;EACnE,MAAM,yBAAS,IAAI,KAQhB;AACH,OAAK,MAAM,KAAK,YAAY;AAC1B,OAAI,CAAC,EAAE,cAAe;GACtB,MAAM,MAAM,GAAG,EAAE,OAAO,GAAG,EAAE,MAAM,GAAG,EAAE;GACxC,MAAM,WAAW,OAAO,IAAI,IAAI;AAChC,OAAI;QACE,CAAC,SAAS,UAAU,SAAS,EAAE,cAAc,CAC/C,UAAS,UAAU,KAAK,EAAE,cAAc;SAG1C,QAAO,IAAI,KAAK;IACd,QAAQ,EAAE;IACV,OAAO,EAAE;IACT,QAAQ,EAAE;IACV,WAAW,CAAC,EAAE,cAAc;IAC7B,CAAC;;AAGN,SAAO;;CAGT,sBACE,OACA,QAC0B;AAC1B,SAAO,KAAK,gBAAgB,MACzB,UACC,MAAM,eAAe,SACrB,MAAM,eAAe,SACrB,MAAM,cAAc,MAAM,MAAM,EAAE,SAAS,OAAO,CACrD;;;;;CAMH,MAAc,oBAAoB,IAAyB;AACzD,OAAK,MAAM,SAAS,KAAK,iBAAiB;AACxC,OAAI,CAACM,gBAAAA,yBAAyB,MAAM,CAClC;AAMF,SAAMC,iBAAAA,UAAU,IAJEP,sBAAAA,aAAa,UAC7B,MAAM,aACN,MAAM,WACP,CAC6B;;;CAIlC,YAAoB,GAAuB,KAAY,SAAgB;AACrE,MAAI,CAAC,EAAE,MACL,QAAO;AAET,MAAI,EAAE,MAAM,aAAa,UACvB,QAAO,GAAG,IAAI,IAAI,EAAE,OAAO,GAAG,CAAC,MAAM,QAAQ,OAAO;AAEtD,MAAI,EAAE,MAAM,aAAa,cACvB,QAAO,GAAG,IAAI,IAAI,EAAE,OAAO,GAAG,CAAC,UAAU,QAAQ,OAAO;AAE1D,SAAO;;CAGT,WAAmB,GAAgB;AACjC,MAAI,CAAC,EAAE,MACL,QAAO;AAET,UAAQ,EAAE,OAAV;GACE,KAAK,qBACH,QAAO;GACT,KAAK,oBACH,QAAO;GAET,QACE,QAAO;;;CAIb,cAAsB,GAAgB;AACpC,MAAI,CAAC,EAAE,IACL,QAAO;AAET,UAAQ,EAAE,KAAV;GACE,KAAK,cACH,QAAO;GACT,KAAK,aACH,QAAO;GAIT,QACE,QAAO;;;CAIb,MAAM,mBACJ,SACA,GACA,SACyC;AACzC,MAAI;AACF,SAAM,KAAK,GAAG,YAAY,OAAO,OAAO;AACtC,UAAM,IAAI,GAAG;AACb,UAAM,KAAK,WAAW,aAAa,GAAG;IACtC,MAAM,QAAQ,CAAC,cAAc;AAC7B,QAAI,WAAW,CAAC,QAAQ,aAAa;AACnC,WAAM,KAAK,UAAU;AACrB,SAAI,KAAK,OAAO,MAGd,OAAM,KAAK,QAAQ;UAGrB,OAAM,KAAK,eAAe;IAE5B,MAAM,EAAE,UAAU,UAAU,QACzB,YAAY,CACZ,QAAQ,MAAM,CACd,YAAY;AAEf,UAAM,GAAG,KAAK,SAAS;IAKvB,MAAM,WAJS,MAAM,GAAG,KACtB,OACA,SAAS,OACV,EACsB,GAAG,cAAc;AACxC,UAAM,IAAI,cAAc,QAAQ;KAChC;WACK,OAAO;AACd,OAAI,iBAAiB,cACnB,QAAO,MAAM;AAEf,SAAM;;AAER,QAAM,IAAI,MAAM,cAAc;;CAGhC,kCAA0C,SAA+B;EACvE,MAAM,+BAGF,IAAI,KAAK;AACb,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,aAAa,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,QAAQ;AACnE,OAAI,SACF,UAAS,QAAQ,KAAK,MAAM;OAE5B,cAAa,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,SAAS;IACjD,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,SAAS,CAAC,MAAM;IACjB,CAAC;;AAGN,SAAO;;CAGT,gBAAwB,SAA8B;EACpD,MAAM,6BAA0B,IAAI,KAAK;EACzC,MAAM,kCAA+B,IAAI,KAAK;EAC9C,MAAM,SAAS,eAAe;AAC9B,cAAY,UAAU,UAAU;GAC9B,MAAM,YAAY,MAAM;AACxB,OAAI,UAEF,KAAI,UAAU,WAAW,OAAO,CAC9B,YAAW,IAAI,UAAU;YAChB,UAAU,SAAS,OAAO,EAAE;IAErC,MAAM,aAAa,UAAU,UAAU,UAAU,QAAQ,OAAO,CAAC;AACjE,eAAW,IAAI,WAAW;SAE1B,iBAAgB,IAAI,UAAU;IAGlC;AACF,SAAO;GACL;GACA;GACD;;CAGH,iCACE,SACA,cACA;AACA,cAAY,UAAU,UAAU;GAC9B,MAAM,YAAY,MAAM;AACxB,OAAI,OAAO,cAAc,UAAU;IACjC,MAAM,iBAAiB,aAAa,IAAI,UAAU;AAClD,QAAI,eACF,OAAM,gBAAgB,eAAe;;IAGzC;;;uDAnkBG,UAAS,QAAQ;AAukB1B,SAAS,YAAY,SAA8B,GAAyB;CAC1E,SAAS,GAAG,MAAW;AACrB,IAAE,KAAK;AACP,MAAI,KAAK,MACP,MAAK,MAAM,KAAK,KAAK,MACnB,IAAG,EAAE;;AAIX,IAAG,QAAQ;;AAmBb,IAAM,gBAAN,MAAuB;CACrB,YAAY,OAA2B;AAAX,OAAA,QAAA;;;AAuB9B,MAAa,UAAU,OAAO,UAAU;AACxC,MAAa,OAAO,OAAO,OAAO"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { PgIdentifier } from "../sql/pg-identifier.cjs";
3
4
  import { PostgresQueryBuilder } from "../sql/builder.cjs";
4
5
  import { Postgres, PostgresExplainStage, PostgresTransaction } from "../sql/database.cjs";
5
6
  import { IndexedTable, Statistics } from "./statistics.cjs";
@@ -11,6 +12,10 @@ type IndexIdentifier = string;
11
12
  type IndexRecommendation = PermutedIndexCandidate & {
12
13
  definition: IndexIdentifier;
13
14
  };
15
+ type IndexToCreate = PermutedIndexCandidate & {
16
+ name: PgIdentifier;
17
+ definition: IndexIdentifier;
18
+ };
14
19
  declare class IndexOptimizer {
15
20
  private readonly db;
16
21
  private readonly statistics;
@@ -107,5 +112,5 @@ type PermutedIndexCandidate = {
107
112
  declare const PROCEED: unique symbol;
108
113
  declare const SKIP: unique symbol;
109
114
  //#endregion
110
- export { IndexIdentifier, IndexOptimizer, IndexRecommendation, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP };
115
+ export { IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP };
111
116
  //# sourceMappingURL=genalgo.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"genalgo.d.cts","names":[],"sources":["../../src/optimizer/genalgo.ts"],"mappings":";;;;;;;;;KAiBY,eAAA;AAAA,KAEA,mBAAA,GAAsB,sBAAA;EAChC,UAAA,EAAY,eAAA;AAAA;AAAA,cAUD,cAAA;EAAA,iBAIQ,EAAA;EAAA,iBACA,UAAA;EAAA,QACT,eAAA;EAAA,iBACS,MAAA;EAAA,OANZ,MAAA;cAGY,EAAA,EAAI,QAAA,EACJ,UAAA,EAAY,UAAA,EACrB,eAAA,EAAiB,YAAA,IACR,MAAA;IACf,KAAA;IACA,KAAA;EAAA;EAIE,GAAA,CACJ,OAAA,EAAS,oBAAA,EACT,OAAA,EAAS,kBAAA,IACT,WAAA,IAAe,EAAA,EAAI,mBAAA,KAAwB,OAAA,SAC1C,OAAA,CAAQ,cAAA;EAiFL,iBAAA,CAAkB,OAAA,EAAS,oBAAA,GAAoB,OAAA;UAiYlC,oBAAA;EAAA;;;;;;;;;;;;;;EA9WnB,gBAAA,CAAiB,CAAA,GAAI,OAAA,EAAS,YAAA,OAAmB,YAAA;EAAA;;;;;EAAA,QAWzC,SAAA;EAAA,QAOA,kBAAA;EA4VE;;;EAAA,QAhTF,eAAA;EAAA,QAiHA,YAAA;EAAA,QA6CA,eAAA;EAAA,QAqBA,sBAAA;EAAA,QAkBA,0BAAA;EAAA,QA8BA,qBAAA;EAjZuB;;;EAAA,QAgajB,mBAAA;EAAA,QAaN,WAAA;EAAA,QAaA,UAAA;EAAA,QAeA,aAAA;EAiBF,kBAAA,CACJ,OAAA,EAAS,oBAAA,EACT,CAAA,IAAK,EAAA,EAAI,mBAAA,KAAwB,OAAA,QACjC,OAAA;IAAY,MAAA;IAAoB,WAAA;EAAA,IAC/B,OAAA;IAAU,IAAA,EAAM,oBAAA;EAAA;EAAA,QAsCX,iCAAA;EAAA,QAoBA,eAAA;EAAA,QAyBA,gCAAA;AAAA;AAAA,KA4BE,cAAA;EAEN,IAAA;EACA,eAAA,EAAiB,oBAAA;EACjB,QAAA;EACA,SAAA;EACA,UAAA,EAAY,GAAA;EACZ,eAAA,EAAiB,GAAA;EACjB,YAAA,EAAc,GAAA,SAAY,mBAAA;EAC1B,WAAA,EAAa,oBAAA;AAAA;EAGb,IAAA;EACA,WAAA,EAAa,oBAAA;AAAA;AAAA,KAOP,kBAAA;EACV,MAAA;EACA,KAAA;EACA,MAAA;EACA,IAAA,GAAO,WAAA;EACP,KAAA;IAAU,QAAA,GAAW,YAAA;EAAA;EACrB,aAAA,GAAgB,aAAA;EAChB,eAAA;AAAA;AAAA,KAGU,sBAAA;EACV,MAAA;EACA,KAAA;EACA,OAAA,EAAS,kBAAA;EAET,KAAA;EACA,WAAA;EACA,OAAA;AAAA;AAAA,cAGW,OAAA;AAAA,cACA,IAAA"}
1
+ {"version":3,"file":"genalgo.d.cts","names":[],"sources":["../../src/optimizer/genalgo.ts"],"mappings":";;;;;;;;;;KAiBY,eAAA;AAAA,KAEA,mBAAA,GAAsB,sBAAA;EAChC,UAAA,EAAY,eAAA;AAAA;AAAA,KAKF,aAAA,GAAgB,sBAAA;EAC1B,IAAA,EAAM,YAAA;EACN,UAAA,EAAY,eAAA;AAAA;AAAA,cAGD,cAAA;EAAA,iBAIQ,EAAA;EAAA,iBACA,UAAA;EAAA,QACT,eAAA;EAAA,iBACS,MAAA;EAAA,OANZ,MAAA;cAGY,EAAA,EAAI,QAAA,EACJ,UAAA,EAAY,UAAA,EACrB,eAAA,EAAiB,YAAA,IACR,MAAA;IACf,KAAA;IACA,KAAA;EAAA;EAIE,GAAA,CACJ,OAAA,EAAS,oBAAA,EACT,OAAA,EAAS,kBAAA,IACT,WAAA,IAAe,EAAA,EAAI,mBAAA,KAAwB,OAAA,SAC1C,OAAA,CAAQ,cAAA;EAmFL,iBAAA,CAAkB,OAAA,EAAS,oBAAA,GAAoB,OAAA;UAiYlC,oBAAA;EAAA;EAxeQ;;;;;;;;;AAG7B;;;;EAuHE,gBAAA,CAAiB,CAAA,GAAI,OAAA,EAAS,YAAA,OAAmB,YAAA;EAjHtB;;;;;EAAA,QA4HnB,SAAA;EAAA,QAOA,kBAAA;EArCyB;;;EAAA,QAiFzB,eAAA;EAAA,QAiHA,YAAA;EAAA,QA6CA,eAAA;EAAA,QAqBA,sBAAA;EAAA,QAkBA,0BAAA;EAAA,QA8BA,qBAAA;EA6EL;;;EAAA,QA9DW,mBAAA;EAAA,QAaN,WAAA;EAAA,QAaA,UAAA;EAAA,QAeA,aAAA;EAiBF,kBAAA,CACJ,OAAA,EAAS,oBAAA,EACT,CAAA,IAAK,EAAA,EAAI,mBAAA,KAAwB,OAAA,QACjC,OAAA;IAAY,MAAA;IAAoB,WAAA;EAAA,IAC/B,OAAA;IAAU,IAAA,EAAM,oBAAA;EAAA;EAAA,QAsCX,iCAAA;EAAA,QAoBA,eAAA;EAAA,QAyBA,gCAAA;AAAA;AAAA,KA4BE,cAAA;EAEN,IAAA;EACA,eAAA,EAAiB,oBAAA;EACjB,QAAA;EACA,SAAA;EACA,UAAA,EAAY,GAAA;EACZ,eAAA,EAAiB,GAAA;EACjB,YAAA,EAAc,GAAA,SAAY,mBAAA;EAC1B,WAAA,EAAa,oBAAA;AAAA;EAGb,IAAA;EACA,WAAA,EAAa,oBAAA;AAAA;AAAA,KAOP,kBAAA;EACV,MAAA;EACA,KAAA;EACA,MAAA;EACA,IAAA,GAAO,WAAA;EACP,KAAA;IAAU,QAAA,GAAW,YAAA;EAAA;EACrB,aAAA,GAAgB,aAAA;EAChB,eAAA;AAAA;AAAA,KAGU,sBAAA;EACV,MAAA;EACA,KAAA;EACA,OAAA,EAAS,kBAAA;EAET,KAAA;EACA,WAAA;EACA,OAAA;AAAA;AAAA,cAGW,OAAA;AAAA,cACA,IAAA"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { PgIdentifier } from "../sql/pg-identifier.mjs";
3
4
  import { PostgresQueryBuilder } from "../sql/builder.mjs";
4
5
  import { Postgres, PostgresExplainStage, PostgresTransaction } from "../sql/database.mjs";
5
6
  import { IndexedTable, Statistics } from "./statistics.mjs";
@@ -11,6 +12,10 @@ type IndexIdentifier = string;
11
12
  type IndexRecommendation = PermutedIndexCandidate & {
12
13
  definition: IndexIdentifier;
13
14
  };
15
+ type IndexToCreate = PermutedIndexCandidate & {
16
+ name: PgIdentifier;
17
+ definition: IndexIdentifier;
18
+ };
14
19
  declare class IndexOptimizer {
15
20
  private readonly db;
16
21
  private readonly statistics;
@@ -107,5 +112,5 @@ type PermutedIndexCandidate = {
107
112
  declare const PROCEED: unique symbol;
108
113
  declare const SKIP: unique symbol;
109
114
  //#endregion
110
- export { IndexIdentifier, IndexOptimizer, IndexRecommendation, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP };
115
+ export { IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, OptimizeResult, PROCEED, PermutedIndexCandidate, RootIndexCandidate, SKIP };
111
116
  //# sourceMappingURL=genalgo.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"genalgo.d.mts","names":[],"sources":["../../src/optimizer/genalgo.ts"],"mappings":";;;;;;;;;KAiBY,eAAA;AAAA,KAEA,mBAAA,GAAsB,sBAAA;EAChC,UAAA,EAAY,eAAA;AAAA;AAAA,cAUD,cAAA;EAAA,iBAIQ,EAAA;EAAA,iBACA,UAAA;EAAA,QACT,eAAA;EAAA,iBACS,MAAA;EAAA,OANZ,MAAA;cAGY,EAAA,EAAI,QAAA,EACJ,UAAA,EAAY,UAAA,EACrB,eAAA,EAAiB,YAAA,IACR,MAAA;IACf,KAAA;IACA,KAAA;EAAA;EAIE,GAAA,CACJ,OAAA,EAAS,oBAAA,EACT,OAAA,EAAS,kBAAA,IACT,WAAA,IAAe,EAAA,EAAI,mBAAA,KAAwB,OAAA,SAC1C,OAAA,CAAQ,cAAA;EAiFL,iBAAA,CAAkB,OAAA,EAAS,oBAAA,GAAoB,OAAA;UAiYlC,oBAAA;EAAA;;;;;;;;;;;;;;EA9WnB,gBAAA,CAAiB,CAAA,GAAI,OAAA,EAAS,YAAA,OAAmB,YAAA;EAAA;;;;;EAAA,QAWzC,SAAA;EAAA,QAOA,kBAAA;EA4VE;;;EAAA,QAhTF,eAAA;EAAA,QAiHA,YAAA;EAAA,QA6CA,eAAA;EAAA,QAqBA,sBAAA;EAAA,QAkBA,0BAAA;EAAA,QA8BA,qBAAA;EAjZuB;;;EAAA,QAgajB,mBAAA;EAAA,QAaN,WAAA;EAAA,QAaA,UAAA;EAAA,QAeA,aAAA;EAiBF,kBAAA,CACJ,OAAA,EAAS,oBAAA,EACT,CAAA,IAAK,EAAA,EAAI,mBAAA,KAAwB,OAAA,QACjC,OAAA;IAAY,MAAA;IAAoB,WAAA;EAAA,IAC/B,OAAA;IAAU,IAAA,EAAM,oBAAA;EAAA;EAAA,QAsCX,iCAAA;EAAA,QAoBA,eAAA;EAAA,QAyBA,gCAAA;AAAA;AAAA,KA4BE,cAAA;EAEN,IAAA;EACA,eAAA,EAAiB,oBAAA;EACjB,QAAA;EACA,SAAA;EACA,UAAA,EAAY,GAAA;EACZ,eAAA,EAAiB,GAAA;EACjB,YAAA,EAAc,GAAA,SAAY,mBAAA;EAC1B,WAAA,EAAa,oBAAA;AAAA;EAGb,IAAA;EACA,WAAA,EAAa,oBAAA;AAAA;AAAA,KAOP,kBAAA;EACV,MAAA;EACA,KAAA;EACA,MAAA;EACA,IAAA,GAAO,WAAA;EACP,KAAA;IAAU,QAAA,GAAW,YAAA;EAAA;EACrB,aAAA,GAAgB,aAAA;EAChB,eAAA;AAAA;AAAA,KAGU,sBAAA;EACV,MAAA;EACA,KAAA;EACA,OAAA,EAAS,kBAAA;EAET,KAAA;EACA,WAAA;EACA,OAAA;AAAA;AAAA,cAGW,OAAA;AAAA,cACA,IAAA"}
1
+ {"version":3,"file":"genalgo.d.mts","names":[],"sources":["../../src/optimizer/genalgo.ts"],"mappings":";;;;;;;;;;KAiBY,eAAA;AAAA,KAEA,mBAAA,GAAsB,sBAAA;EAChC,UAAA,EAAY,eAAA;AAAA;AAAA,KAKF,aAAA,GAAgB,sBAAA;EAC1B,IAAA,EAAM,YAAA;EACN,UAAA,EAAY,eAAA;AAAA;AAAA,cAGD,cAAA;EAAA,iBAIQ,EAAA;EAAA,iBACA,UAAA;EAAA,QACT,eAAA;EAAA,iBACS,MAAA;EAAA,OANZ,MAAA;cAGY,EAAA,EAAI,QAAA,EACJ,UAAA,EAAY,UAAA,EACrB,eAAA,EAAiB,YAAA,IACR,MAAA;IACf,KAAA;IACA,KAAA;EAAA;EAIE,GAAA,CACJ,OAAA,EAAS,oBAAA,EACT,OAAA,EAAS,kBAAA,IACT,WAAA,IAAe,EAAA,EAAI,mBAAA,KAAwB,OAAA,SAC1C,OAAA,CAAQ,cAAA;EAmFL,iBAAA,CAAkB,OAAA,EAAS,oBAAA,GAAoB,OAAA;UAiYlC,oBAAA;EAAA;EAxeQ;;;;;;;;;AAG7B;;;;EAuHE,gBAAA,CAAiB,CAAA,GAAI,OAAA,EAAS,YAAA,OAAmB,YAAA;EAjHtB;;;;;EAAA,QA4HnB,SAAA;EAAA,QAOA,kBAAA;EArCyB;;;EAAA,QAiFzB,eAAA;EAAA,QAiHA,YAAA;EAAA,QA6CA,eAAA;EAAA,QAqBA,sBAAA;EAAA,QAkBA,0BAAA;EAAA,QA8BA,qBAAA;EA6EL;;;EAAA,QA9DW,mBAAA;EAAA,QAaN,WAAA;EAAA,QAaA,UAAA;EAAA,QAeA,aAAA;EAiBF,kBAAA,CACJ,OAAA,EAAS,oBAAA,EACT,CAAA,IAAK,EAAA,EAAI,mBAAA,KAAwB,OAAA,QACjC,OAAA;IAAY,MAAA;IAAoB,WAAA;EAAA,IAC/B,OAAA;IAAU,IAAA,EAAM,oBAAA;EAAA;EAAA,QAsCX,iCAAA;EAAA,QAoBA,eAAA;EAAA,QAyBA,gCAAA;AAAA;AAAA,KA4BE,cAAA;EAEN,IAAA;EACA,eAAA,EAAiB,oBAAA;EACjB,QAAA;EACA,SAAA;EACA,UAAA,EAAY,GAAA;EACZ,eAAA,EAAiB,GAAA;EACjB,YAAA,EAAc,GAAA,SAAY,mBAAA;EAC1B,WAAA,EAAa,oBAAA;AAAA;EAGb,IAAA;EACA,WAAA,EAAa,oBAAA;AAAA;AAAA,KAOP,kBAAA;EACV,MAAA;EACA,KAAA;EACA,MAAA;EACA,IAAA,GAAO,WAAA;EACP,KAAA;IAAU,QAAA,GAAW,YAAA;EAAA;EACrB,aAAA,GAAgB,aAAA;EAChB,eAAA;AAAA;AAAA,KAGU,sBAAA;EACV,MAAA;EACA,KAAA;EACA,OAAA,EAAS,kBAAA;EAET,KAAA;EACA,WAAA;EACA,OAAA;AAAA;AAAA,cAGW,OAAA;AAAA,cACA,IAAA"}
@@ -37,6 +37,7 @@ var IndexOptimizer = class IndexOptimizer {
37
37
  explainPlan: baseExplain.Plan
38
38
  };
39
39
  }
40
+ this.statistics.setAdditionalIndexes(toCreate);
40
41
  const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
41
42
  if (beforeQuery) await beforeQuery(tx);
42
43
  for (const permutation of toCreate) {
@@ -44,6 +45,7 @@ var IndexOptimizer = class IndexOptimizer {
44
45
  await tx.exec(createIndex);
45
46
  }
46
47
  });
48
+ this.statistics.setAdditionalIndexes([]);
47
49
  const finalCost = Number(finalExplain.Plan["Total Cost"]);
48
50
  if (this.config.debug) console.dir(finalExplain, { depth: null });
49
51
  const deltaPercentage = (baseCost - finalCost) / baseCost * 100;
@@ -1 +1 @@
1
- {"version":3,"file":"genalgo.mjs","names":[],"sources":["../../src/optimizer/genalgo.ts"],"sourcesContent":["import type { NullTestType } from \"@pgsql/types\";\nimport { blue, gray, green, magenta, red, yellow } from \"colorette\";\nimport type { JsonbOperator } from \"../sql/analyzer.js\";\nimport type { SortContext } from \"../sql/analyzer.js\";\nimport { PostgresQueryBuilder } from \"../sql/builder.js\";\nimport {\n dropIndex,\n type Postgres,\n type PostgresExplainResult,\n type PostgresExplainStage,\n type PostgresTransaction,\n} from \"../sql/database.js\";\nimport { isIndexProbablyDroppable } from \"../sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"../sql/permutations.js\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexedTable, Statistics } from \"./statistics.js\";\n\nexport type IndexIdentifier = string;\n\nexport type IndexRecommendation = PermutedIndexCandidate & {\n definition: IndexIdentifier;\n};\n\ntype Color = (a: string) => string;\n\ntype IndexToCreate = PermutedIndexCandidate & {\n name: PgIdentifier;\n definition: IndexIdentifier;\n};\n\nexport class IndexOptimizer {\n static prefix = \"__qd_\";\n\n constructor(\n private readonly db: Postgres,\n private readonly statistics: Statistics,\n private existingIndexes: IndexedTable[],\n private readonly config: {\n trace?: boolean;\n debug?: boolean;\n } = {},\n ) {}\n\n async run(\n builder: PostgresQueryBuilder,\n indexes: RootIndexCandidate[],\n beforeQuery?: (tx: PostgresTransaction) => Promise<void>,\n ): Promise<OptimizeResult> {\n const baseExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n });\n const baseCost: number = Number(baseExplain.Plan[\"Total Cost\"]);\n if (baseCost === 0) {\n return {\n kind: \"zero_cost_plan\",\n explainPlan: baseExplain.Plan,\n };\n }\n const toCreate = this.indexesToCreate(indexes);\n if (toCreate.length === 0) {\n // No indexes to try: the 2nd EXPLAIN would be identical to the 1st,\n // so skip it. On large tables this saves a full re-run of the query.\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n return {\n kind: \"ok\",\n baseCost,\n finalCost: baseCost,\n newIndexes: new Set<string>(),\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes: new Map(),\n baseExplainPlan: baseExplain.Plan,\n explainPlan: baseExplain.Plan,\n };\n }\n const finalExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n\n // Then create recommended indexes\n for (const permutation of toCreate) {\n const createIndex = PostgresQueryBuilder.createIndex(\n permutation.definition,\n permutation.name,\n )\n .introspect()\n .build();\n\n await tx.exec(createIndex);\n }\n });\n const finalCost = Number(finalExplain.Plan[\"Total Cost\"]);\n if (this.config.debug) {\n console.dir(finalExplain, { depth: null });\n }\n const deltaPercentage = ((baseCost - finalCost) / baseCost) * 100;\n if (finalCost < baseCost) {\n console.log(\n ` 🎉🎉🎉 ${green(`+${deltaPercentage.toFixed(2).padStart(5, \"0\")}%`)}`,\n );\n } else if (finalCost > baseCost) {\n console.log(\n `${red(\n `-${Math.abs(deltaPercentage).toFixed(2).padStart(5, \"0\")}%`,\n )} ${gray(\"If there's a better index, we haven't tried it\")}`,\n );\n }\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n const finalIndexes = this.findUsedIndexes(finalExplain.Plan);\n const triedIndexes = new Map(\n toCreate.map((index) => [index.name.toString(), index]),\n );\n this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);\n\n return {\n kind: \"ok\",\n baseCost,\n finalCost,\n newIndexes: finalIndexes.newIndexes,\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes,\n baseExplainPlan: baseExplain.Plan,\n explainPlan: finalExplain.Plan,\n };\n }\n\n async runWithoutIndexes(builder: PostgresQueryBuilder) {\n return await this.testQueryWithStats(builder, async (tx) => {\n await this.dropExistingIndexes(tx);\n });\n }\n\n /**\n * Given the current indexes in the optimizer, transform them in some\n * way to change which indexes will be assumed to exist when optimizing\n *\n * @example\n * ```\n * // resets indexes\n * optimizer.transformIndexes(() => [])\n *\n * // adds new index\n * optimizer.transformIndexes(indexes => [...indexes, newIndex])\n * ```\n */\n transformIndexes(f: (indexes: IndexedTable[]) => IndexedTable[]) {\n const newIndexes = f(this.existingIndexes);\n this.existingIndexes = newIndexes;\n return this;\n }\n\n /**\n * Postgres has a limit of 63 characters for index names.\n * So we use this to make sure we don't derive it from a list of columns that can\n * overflow that limit.\n */\n private indexName(): PgIdentifier {\n const indexName =\n IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);\n return PgIdentifier.fromString(indexName);\n }\n\n // TODO: this doesn't belong in the optimizer\n private indexAlreadyExists(\n table: string,\n columns: RootIndexCandidate[],\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"btree\" &&\n index.table_name === table &&\n index.index_columns.length === columns.length &&\n index.index_columns.every((c, i) => {\n if (columns[i].column !== c.name) {\n return false;\n }\n\n // we should assume any index with `WHERE`\n // can't be counted as a duplicate\n if (columns[i].where) {\n return false;\n }\n\n if (columns[i].sort) {\n switch (columns[i].sort.dir) {\n // Sorting is ASC by default in postgres\n case \"SORTBY_DEFAULT\":\n case \"SORTBY_ASC\":\n if (c.order !== \"ASC\") {\n return false;\n }\n break;\n case \"SORTBY_DESC\":\n if (c.order !== \"DESC\") {\n return false;\n }\n break;\n }\n }\n return true;\n }),\n );\n }\n\n /**\n * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]\n **/\n private indexesToCreate(\n rootCandidates: RootIndexCandidate[],\n ): IndexToCreate[] {\n const expressionCandidates = rootCandidates.filter(\n (c) => c.jsonbExtraction,\n );\n const btreeCandidates = rootCandidates.filter(\n (c) => !c.jsonbOperator && !c.jsonbExtraction,\n );\n const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);\n\n const nextStage: IndexToCreate[] = [];\n\n const permutedIndexes =\n this.groupPotentialIndexColumnsByTable(btreeCandidates);\n for (const permutation of permutedIndexes.values()) {\n const { table: rawTable, schema: rawSchema, columns } = permutation;\n const permutations = permutationsWithDescendingLength(columns);\n for (const columns of permutations) {\n // TODO: accept PgIdentifier values instead\n // required refactoring `PermutedIndexCandidate`\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n const existingIndex = this.indexAlreadyExists(\n table.toString(),\n columns,\n );\n if (existingIndex) {\n continue;\n }\n const indexName = this.indexName();\n\n const definition = this.toDefinition({ table, schema, columns }).raw;\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns,\n definition,\n });\n }\n }\n\n const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);\n for (const group of ginGroups.values()) {\n const { schema: rawSchema, table: rawTable, column, operators } = group;\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n\n // jsonb_path_ops is smaller/faster but only supports @>.\n // All other operators (key-existence and jsonpath) need the full jsonb_ops.\n const needsFullOps = operators.some((op) => op !== \"@>\");\n const opclass = needsFullOps ? undefined : \"jsonb_path_ops\";\n\n const existingGin = this.ginIndexAlreadyExists(table.toString(), column);\n if (existingGin) {\n continue;\n }\n\n const indexName = this.indexName();\n const candidate: RootIndexCandidate = {\n schema: rawSchema,\n table: rawTable,\n column,\n };\n const definition = this.toGinDefinition({\n table,\n schema,\n column: PgIdentifier.fromString(column),\n opclass,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n indexMethod: \"gin\",\n opclass,\n });\n }\n\n // Expression B-tree indexes for JSONB path extraction (-> / ->>)\n const seenExpressions = new Set<string>();\n for (const candidate of expressionCandidates) {\n const expression = candidate.jsonbExtraction!;\n const key = `${candidate.schema}.${candidate.table}.${expression}`;\n if (seenExpressions.has(key)) continue;\n seenExpressions.add(key);\n\n const schema = PgIdentifier.fromString(candidate.schema);\n const table = PgIdentifier.fromString(candidate.table);\n const indexName = this.indexName();\n const definition = this.toExpressionDefinition({\n table,\n schema,\n expression,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n });\n }\n\n return nextStage;\n }\n\n private toDefinition({\n schema,\n table,\n columns,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n columns: RootIndexCandidate[];\n }) {\n const make = (col: Color, order: Color, _where: Color, _keyword: Color) => {\n // TODO: refactor all of this class to accept PgIdentifiers\n let fullyQualifiedTable: PgIdentifier;\n\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const baseColumn = `${fullyQualifiedTable}(${columns\n .map((c) => {\n const column = PgIdentifier.fromString(c.column);\n const direction = c.sort && this.sortDirection(c.sort);\n const nulls = c.sort && this.nullsOrder(c.sort);\n let sort = col(column.toString());\n if (direction) {\n sort += ` ${order(direction)}`;\n }\n if (nulls) {\n sort += ` ${order(nulls)}`;\n }\n return sort;\n })\n .join(\", \")})`;\n // TODO: add support for generating partial indexes\n // if (clauses.length > 0) {\n // return `${baseColumn} ${where(\"where\")} ${clauses.join(\" and \")}`;\n // }\n return baseColumn;\n };\n const id: Color = (a) => a;\n const raw = make(id, id, id, id);\n const colored = make(green, yellow, magenta, blue);\n return { raw, colored };\n }\n\n private toGinDefinition({\n schema,\n table,\n column,\n opclass,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n column: PgIdentifier;\n opclass?: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const opclassSuffix = opclass ? ` ${opclass}` : \"\";\n return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;\n }\n\n private toExpressionDefinition({\n schema,\n table,\n expression,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n expression: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n return `${fullyQualifiedTable}((${expression}))`;\n }\n\n private groupGinCandidatesByColumn(candidates: RootIndexCandidate[]) {\n const groups = new Map<\n string,\n {\n schema: string;\n table: string;\n column: string;\n operators: JsonbOperator[];\n }\n >();\n for (const c of candidates) {\n if (!c.jsonbOperator) continue;\n const key = `${c.schema}.${c.table}.${c.column}`;\n const existing = groups.get(key);\n if (existing) {\n if (!existing.operators.includes(c.jsonbOperator)) {\n existing.operators.push(c.jsonbOperator);\n }\n } else {\n groups.set(key, {\n schema: c.schema,\n table: c.table,\n column: c.column,\n operators: [c.jsonbOperator],\n });\n }\n }\n return groups;\n }\n\n private ginIndexAlreadyExists(\n table: string,\n column: string,\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"gin\" &&\n index.table_name === table &&\n index.index_columns.some((c) => c.name === column),\n );\n }\n\n /**\n * Drop indexes that can be dropped. Ignore the ones that can't\n */\n private async dropExistingIndexes(tx: PostgresTransaction) {\n for (const index of this.existingIndexes) {\n if (!isIndexProbablyDroppable(index)) {\n continue;\n }\n const indexName = PgIdentifier.fromParts(\n index.schema_name,\n index.index_name,\n );\n await dropIndex(tx, indexName);\n }\n }\n\n private whereClause(c: RootIndexCandidate, col: Color, keyword: Color) {\n if (!c.where) {\n return \"\";\n }\n if (c.where.nulltest === \"IS_NULL\") {\n return `${col(`\"${c.column}\"`)} is ${keyword(\"null\")}`;\n }\n if (c.where.nulltest === \"IS_NOT_NULL\") {\n return `${col(`\"${c.column}\"`)} is not ${keyword(\"null\")}`;\n }\n return \"\";\n }\n\n private nullsOrder(s: SortContext) {\n if (!s.nulls) {\n return \"\";\n }\n switch (s.nulls) {\n case \"SORTBY_NULLS_FIRST\":\n return \"nulls first\";\n case \"SORTBY_NULLS_LAST\":\n return \"nulls last\";\n case \"SORTBY_NULLS_DEFAULT\":\n default:\n return \"\";\n }\n }\n\n private sortDirection(s: SortContext) {\n if (!s.dir) {\n return \"\";\n }\n switch (s.dir) {\n case \"SORTBY_DESC\":\n return \"desc\";\n case \"SORTBY_ASC\":\n return \"asc\";\n case \"SORTBY_DEFAULT\":\n // god help us if we ever run into this\n case \"SORTBY_USING\":\n default:\n return \"\";\n }\n }\n\n async testQueryWithStats(\n builder: PostgresQueryBuilder,\n f?: (tx: PostgresTransaction) => Promise<void>,\n options?: { params?: unknown[]; genericPlan?: boolean },\n ): Promise<{ Plan: PostgresExplainStage }> {\n try {\n await this.db.transaction(async (tx) => {\n await f?.(tx);\n await this.statistics.restoreStats(tx);\n const flags = [\"format json\"];\n if (options && !options.genericPlan) {\n flags.push(\"analyze\");\n if (this.config.trace) {\n // trace can only be used alongside analyze\n // since it depends on the results of the query execution\n flags.push(\"trace\");\n }\n } else {\n flags.push(\"generic_plan\");\n }\n const { commands, query } = builder\n .introspect()\n .explain(flags)\n .buildParts();\n // this is done in a separate step to prevent sending multiple commands when using parameters\n await tx.exec(commands);\n const result = await tx.exec<PostgresExplainResult>(\n query,\n options?.params,\n );\n const explain = result[0][\"QUERY PLAN\"][0];\n throw new RollbackError(explain);\n });\n } catch (error) {\n if (error instanceof RollbackError) {\n return error.value;\n }\n throw error;\n }\n throw new Error(\"Unreachable\");\n }\n\n private groupPotentialIndexColumnsByTable(indexes: RootIndexCandidate[]) {\n const tableColumns: Map<\n string,\n { schema: string; table: string; columns: RootIndexCandidate[] }\n > = new Map();\n for (const index of indexes) {\n const existing = tableColumns.get(`${index.schema}.${index.table}`);\n if (existing) {\n existing.columns.push(index);\n } else {\n tableColumns.set(`${index.schema}.${index.table}`, {\n table: index.table,\n schema: index.schema,\n columns: [index],\n });\n }\n }\n return tableColumns;\n }\n\n private findUsedIndexes(explain: Record<string, any>) {\n const newIndexes: Set<string> = new Set();\n const existingIndexes: Set<string> = new Set();\n const prefix = IndexOptimizer.prefix;\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (indexName) {\n // Check for prefix at start or embedded (for hypertable chunk indexes like _hyper_1_1_chunk___qd_xxx)\n if (indexName.startsWith(prefix)) {\n newIndexes.add(indexName);\n } else if (indexName.includes(prefix)) {\n // Extract the actual index name from chunk-prefixed names (e.g., _hyper_1_1_chunk___qd_xxx -> __qd_xxx)\n const actualName = indexName.substring(indexName.indexOf(prefix));\n newIndexes.add(actualName);\n } else {\n existingIndexes.add(indexName);\n }\n }\n });\n return {\n newIndexes,\n existingIndexes,\n };\n }\n\n private replaceUsedIndexesWithDefinition(\n explain: Record<string, any>,\n triedIndexes: Map<string, IndexRecommendation>,\n ) {\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (typeof indexName === \"string\") {\n const recommendation = triedIndexes.get(indexName);\n if (recommendation) {\n stage[\"Index Name\"] = recommendation.definition;\n }\n }\n });\n }\n}\n\nfunction walkExplain(explain: Record<string, any>, f: (stage: any) => void) {\n function go(plan: any) {\n f(plan);\n if (plan.Plans) {\n for (const p of plan.Plans) {\n go(p);\n }\n }\n }\n go(explain);\n}\n\nexport type OptimizeResult =\n | {\n kind: \"ok\";\n baseExplainPlan: PostgresExplainStage;\n baseCost: number;\n finalCost: number;\n newIndexes: Set<string>;\n existingIndexes: Set<string>;\n triedIndexes: Map<string, IndexRecommendation>;\n explainPlan: PostgresExplainStage;\n }\n | {\n kind: \"zero_cost_plan\";\n explainPlan: PostgresExplainStage;\n };\n\nclass RollbackError<T> {\n constructor(public readonly value?: T) {}\n}\n\nexport type RootIndexCandidate = {\n schema: string;\n table: string;\n column: string;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n};\n\nexport type PermutedIndexCandidate = {\n schema: string;\n table: string;\n columns: RootIndexCandidate[];\n // TODO: functional indexes\n where?: string;\n indexMethod?: \"btree\" | \"gin\";\n opclass?: string;\n};\n\nexport const PROCEED = Symbol(\"PROCEED\");\nexport const SKIP = Symbol(\"SKIP\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"genalgo.mjs","names":[],"sources":["../../src/optimizer/genalgo.ts"],"sourcesContent":["import type { NullTestType } from \"@pgsql/types\";\nimport { blue, gray, green, magenta, red, yellow } from \"colorette\";\nimport type { JsonbOperator } from \"../sql/analyzer.js\";\nimport type { SortContext } from \"../sql/analyzer.js\";\nimport { PostgresQueryBuilder } from \"../sql/builder.js\";\nimport {\n dropIndex,\n type Postgres,\n type PostgresExplainResult,\n type PostgresExplainStage,\n type PostgresTransaction,\n} from \"../sql/database.js\";\nimport { isIndexProbablyDroppable } from \"../sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"../sql/permutations.js\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexedTable, Statistics } from \"./statistics.js\";\n\nexport type IndexIdentifier = string;\n\nexport type IndexRecommendation = PermutedIndexCandidate & {\n definition: IndexIdentifier;\n};\n\ntype Color = (a: string) => string;\n\nexport type IndexToCreate = PermutedIndexCandidate & {\n name: PgIdentifier;\n definition: IndexIdentifier;\n};\n\nexport class IndexOptimizer {\n static prefix = \"__qd_\";\n\n constructor(\n private readonly db: Postgres,\n private readonly statistics: Statistics,\n private existingIndexes: IndexedTable[],\n private readonly config: {\n trace?: boolean;\n debug?: boolean;\n } = {},\n ) {}\n\n async run(\n builder: PostgresQueryBuilder,\n indexes: RootIndexCandidate[],\n beforeQuery?: (tx: PostgresTransaction) => Promise<void>,\n ): Promise<OptimizeResult> {\n const baseExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n });\n const baseCost: number = Number(baseExplain.Plan[\"Total Cost\"]);\n if (baseCost === 0) {\n return {\n kind: \"zero_cost_plan\",\n explainPlan: baseExplain.Plan,\n };\n }\n const toCreate = this.indexesToCreate(indexes);\n if (toCreate.length === 0) {\n // No indexes to try: the 2nd EXPLAIN would be identical to the 1st,\n // so skip it. On large tables this saves a full re-run of the query.\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n return {\n kind: \"ok\",\n baseCost,\n finalCost: baseCost,\n newIndexes: new Set<string>(),\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes: new Map(),\n baseExplainPlan: baseExplain.Plan,\n explainPlan: baseExplain.Plan,\n };\n }\n this.statistics.setAdditionalIndexes(toCreate);\n const finalExplain = await this.testQueryWithStats(builder, async (tx) => {\n if (beforeQuery) {\n await beforeQuery(tx);\n }\n\n // Then create recommended indexes\n for (const permutation of toCreate) {\n const createIndex = PostgresQueryBuilder.createIndex(\n permutation.definition,\n permutation.name,\n )\n .introspect()\n .build();\n\n await tx.exec(createIndex);\n }\n });\n this.statistics.setAdditionalIndexes([]);\n const finalCost = Number(finalExplain.Plan[\"Total Cost\"]);\n if (this.config.debug) {\n console.dir(finalExplain, { depth: null });\n }\n const deltaPercentage = ((baseCost - finalCost) / baseCost) * 100;\n if (finalCost < baseCost) {\n console.log(\n ` 🎉🎉🎉 ${green(`+${deltaPercentage.toFixed(2).padStart(5, \"0\")}%`)}`,\n );\n } else if (finalCost > baseCost) {\n console.log(\n `${red(\n `-${Math.abs(deltaPercentage).toFixed(2).padStart(5, \"0\")}%`,\n )} ${gray(\"If there's a better index, we haven't tried it\")}`,\n );\n }\n const baseIndexes = this.findUsedIndexes(baseExplain.Plan);\n const finalIndexes = this.findUsedIndexes(finalExplain.Plan);\n const triedIndexes = new Map(\n toCreate.map((index) => [index.name.toString(), index]),\n );\n this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);\n\n return {\n kind: \"ok\",\n baseCost,\n finalCost,\n newIndexes: finalIndexes.newIndexes,\n existingIndexes: baseIndexes.existingIndexes,\n triedIndexes,\n baseExplainPlan: baseExplain.Plan,\n explainPlan: finalExplain.Plan,\n };\n }\n\n async runWithoutIndexes(builder: PostgresQueryBuilder) {\n return await this.testQueryWithStats(builder, async (tx) => {\n await this.dropExistingIndexes(tx);\n });\n }\n\n /**\n * Given the current indexes in the optimizer, transform them in some\n * way to change which indexes will be assumed to exist when optimizing\n *\n * @example\n * ```\n * // resets indexes\n * optimizer.transformIndexes(() => [])\n *\n * // adds new index\n * optimizer.transformIndexes(indexes => [...indexes, newIndex])\n * ```\n */\n transformIndexes(f: (indexes: IndexedTable[]) => IndexedTable[]) {\n const newIndexes = f(this.existingIndexes);\n this.existingIndexes = newIndexes;\n return this;\n }\n\n /**\n * Postgres has a limit of 63 characters for index names.\n * So we use this to make sure we don't derive it from a list of columns that can\n * overflow that limit.\n */\n private indexName(): PgIdentifier {\n const indexName =\n IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);\n return PgIdentifier.fromString(indexName);\n }\n\n // TODO: this doesn't belong in the optimizer\n private indexAlreadyExists(\n table: string,\n columns: RootIndexCandidate[],\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"btree\" &&\n index.table_name === table &&\n index.index_columns.length === columns.length &&\n index.index_columns.every((c, i) => {\n if (columns[i].column !== c.name) {\n return false;\n }\n\n // we should assume any index with `WHERE`\n // can't be counted as a duplicate\n if (columns[i].where) {\n return false;\n }\n\n if (columns[i].sort) {\n switch (columns[i].sort.dir) {\n // Sorting is ASC by default in postgres\n case \"SORTBY_DEFAULT\":\n case \"SORTBY_ASC\":\n if (c.order !== \"ASC\") {\n return false;\n }\n break;\n case \"SORTBY_DESC\":\n if (c.order !== \"DESC\") {\n return false;\n }\n break;\n }\n }\n return true;\n }),\n );\n }\n\n /**\n * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]\n **/\n private indexesToCreate(\n rootCandidates: RootIndexCandidate[],\n ): IndexToCreate[] {\n const expressionCandidates = rootCandidates.filter(\n (c) => c.jsonbExtraction,\n );\n const btreeCandidates = rootCandidates.filter(\n (c) => !c.jsonbOperator && !c.jsonbExtraction,\n );\n const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);\n\n const nextStage: IndexToCreate[] = [];\n\n const permutedIndexes =\n this.groupPotentialIndexColumnsByTable(btreeCandidates);\n for (const permutation of permutedIndexes.values()) {\n const { table: rawTable, schema: rawSchema, columns } = permutation;\n const permutations = permutationsWithDescendingLength(columns);\n for (const columns of permutations) {\n // TODO: accept PgIdentifier values instead\n // required refactoring `PermutedIndexCandidate`\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n const existingIndex = this.indexAlreadyExists(\n table.toString(),\n columns,\n );\n if (existingIndex) {\n continue;\n }\n const indexName = this.indexName();\n\n const definition = this.toDefinition({ table, schema, columns }).raw;\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns,\n definition,\n });\n }\n }\n\n const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);\n for (const group of ginGroups.values()) {\n const { schema: rawSchema, table: rawTable, column, operators } = group;\n const schema = PgIdentifier.fromString(rawSchema);\n const table = PgIdentifier.fromString(rawTable);\n\n // jsonb_path_ops is smaller/faster but only supports @>.\n // All other operators (key-existence and jsonpath) need the full jsonb_ops.\n const needsFullOps = operators.some((op) => op !== \"@>\");\n const opclass = needsFullOps ? undefined : \"jsonb_path_ops\";\n\n const existingGin = this.ginIndexAlreadyExists(table.toString(), column);\n if (existingGin) {\n continue;\n }\n\n const indexName = this.indexName();\n const candidate: RootIndexCandidate = {\n schema: rawSchema,\n table: rawTable,\n column,\n };\n const definition = this.toGinDefinition({\n table,\n schema,\n column: PgIdentifier.fromString(column),\n opclass,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n indexMethod: \"gin\",\n opclass,\n });\n }\n\n // Expression B-tree indexes for JSONB path extraction (-> / ->>)\n const seenExpressions = new Set<string>();\n for (const candidate of expressionCandidates) {\n const expression = candidate.jsonbExtraction!;\n const key = `${candidate.schema}.${candidate.table}.${expression}`;\n if (seenExpressions.has(key)) continue;\n seenExpressions.add(key);\n\n const schema = PgIdentifier.fromString(candidate.schema);\n const table = PgIdentifier.fromString(candidate.table);\n const indexName = this.indexName();\n const definition = this.toExpressionDefinition({\n table,\n schema,\n expression,\n });\n\n nextStage.push({\n name: indexName,\n schema: schema.toString(),\n table: table.toString(),\n columns: [candidate],\n definition,\n });\n }\n\n return nextStage;\n }\n\n private toDefinition({\n schema,\n table,\n columns,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n columns: RootIndexCandidate[];\n }) {\n const make = (col: Color, order: Color, _where: Color, _keyword: Color) => {\n // TODO: refactor all of this class to accept PgIdentifiers\n let fullyQualifiedTable: PgIdentifier;\n\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const baseColumn = `${fullyQualifiedTable}(${columns\n .map((c) => {\n const column = PgIdentifier.fromString(c.column);\n const direction = c.sort && this.sortDirection(c.sort);\n const nulls = c.sort && this.nullsOrder(c.sort);\n let sort = col(column.toString());\n if (direction) {\n sort += ` ${order(direction)}`;\n }\n if (nulls) {\n sort += ` ${order(nulls)}`;\n }\n return sort;\n })\n .join(\", \")})`;\n // TODO: add support for generating partial indexes\n // if (clauses.length > 0) {\n // return `${baseColumn} ${where(\"where\")} ${clauses.join(\" and \")}`;\n // }\n return baseColumn;\n };\n const id: Color = (a) => a;\n const raw = make(id, id, id, id);\n const colored = make(green, yellow, magenta, blue);\n return { raw, colored };\n }\n\n private toGinDefinition({\n schema,\n table,\n column,\n opclass,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n column: PgIdentifier;\n opclass?: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n const opclassSuffix = opclass ? ` ${opclass}` : \"\";\n return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;\n }\n\n private toExpressionDefinition({\n schema,\n table,\n expression,\n }: {\n schema: PgIdentifier;\n table: PgIdentifier;\n expression: string;\n }): string {\n let fullyQualifiedTable: PgIdentifier;\n if (schema.toString() === \"public\") {\n fullyQualifiedTable = table;\n } else {\n fullyQualifiedTable = PgIdentifier.fromParts(schema, table);\n }\n return `${fullyQualifiedTable}((${expression}))`;\n }\n\n private groupGinCandidatesByColumn(candidates: RootIndexCandidate[]) {\n const groups = new Map<\n string,\n {\n schema: string;\n table: string;\n column: string;\n operators: JsonbOperator[];\n }\n >();\n for (const c of candidates) {\n if (!c.jsonbOperator) continue;\n const key = `${c.schema}.${c.table}.${c.column}`;\n const existing = groups.get(key);\n if (existing) {\n if (!existing.operators.includes(c.jsonbOperator)) {\n existing.operators.push(c.jsonbOperator);\n }\n } else {\n groups.set(key, {\n schema: c.schema,\n table: c.table,\n column: c.column,\n operators: [c.jsonbOperator],\n });\n }\n }\n return groups;\n }\n\n private ginIndexAlreadyExists(\n table: string,\n column: string,\n ): IndexedTable | undefined {\n return this.existingIndexes.find(\n (index) =>\n index.index_type === \"gin\" &&\n index.table_name === table &&\n index.index_columns.some((c) => c.name === column),\n );\n }\n\n /**\n * Drop indexes that can be dropped. Ignore the ones that can't\n */\n private async dropExistingIndexes(tx: PostgresTransaction) {\n for (const index of this.existingIndexes) {\n if (!isIndexProbablyDroppable(index)) {\n continue;\n }\n const indexName = PgIdentifier.fromParts(\n index.schema_name,\n index.index_name,\n );\n await dropIndex(tx, indexName);\n }\n }\n\n private whereClause(c: RootIndexCandidate, col: Color, keyword: Color) {\n if (!c.where) {\n return \"\";\n }\n if (c.where.nulltest === \"IS_NULL\") {\n return `${col(`\"${c.column}\"`)} is ${keyword(\"null\")}`;\n }\n if (c.where.nulltest === \"IS_NOT_NULL\") {\n return `${col(`\"${c.column}\"`)} is not ${keyword(\"null\")}`;\n }\n return \"\";\n }\n\n private nullsOrder(s: SortContext) {\n if (!s.nulls) {\n return \"\";\n }\n switch (s.nulls) {\n case \"SORTBY_NULLS_FIRST\":\n return \"nulls first\";\n case \"SORTBY_NULLS_LAST\":\n return \"nulls last\";\n case \"SORTBY_NULLS_DEFAULT\":\n default:\n return \"\";\n }\n }\n\n private sortDirection(s: SortContext) {\n if (!s.dir) {\n return \"\";\n }\n switch (s.dir) {\n case \"SORTBY_DESC\":\n return \"desc\";\n case \"SORTBY_ASC\":\n return \"asc\";\n case \"SORTBY_DEFAULT\":\n // god help us if we ever run into this\n case \"SORTBY_USING\":\n default:\n return \"\";\n }\n }\n\n async testQueryWithStats(\n builder: PostgresQueryBuilder,\n f?: (tx: PostgresTransaction) => Promise<void>,\n options?: { params?: unknown[]; genericPlan?: boolean },\n ): Promise<{ Plan: PostgresExplainStage }> {\n try {\n await this.db.transaction(async (tx) => {\n await f?.(tx);\n await this.statistics.restoreStats(tx);\n const flags = [\"format json\"];\n if (options && !options.genericPlan) {\n flags.push(\"analyze\");\n if (this.config.trace) {\n // trace can only be used alongside analyze\n // since it depends on the results of the query execution\n flags.push(\"trace\");\n }\n } else {\n flags.push(\"generic_plan\");\n }\n const { commands, query } = builder\n .introspect()\n .explain(flags)\n .buildParts();\n // this is done in a separate step to prevent sending multiple commands when using parameters\n await tx.exec(commands);\n const result = await tx.exec<PostgresExplainResult>(\n query,\n options?.params,\n );\n const explain = result[0][\"QUERY PLAN\"][0];\n throw new RollbackError(explain);\n });\n } catch (error) {\n if (error instanceof RollbackError) {\n return error.value;\n }\n throw error;\n }\n throw new Error(\"Unreachable\");\n }\n\n private groupPotentialIndexColumnsByTable(indexes: RootIndexCandidate[]) {\n const tableColumns: Map<\n string,\n { schema: string; table: string; columns: RootIndexCandidate[] }\n > = new Map();\n for (const index of indexes) {\n const existing = tableColumns.get(`${index.schema}.${index.table}`);\n if (existing) {\n existing.columns.push(index);\n } else {\n tableColumns.set(`${index.schema}.${index.table}`, {\n table: index.table,\n schema: index.schema,\n columns: [index],\n });\n }\n }\n return tableColumns;\n }\n\n private findUsedIndexes(explain: Record<string, any>) {\n const newIndexes: Set<string> = new Set();\n const existingIndexes: Set<string> = new Set();\n const prefix = IndexOptimizer.prefix;\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (indexName) {\n // Check for prefix at start or embedded (for hypertable chunk indexes like _hyper_1_1_chunk___qd_xxx)\n if (indexName.startsWith(prefix)) {\n newIndexes.add(indexName);\n } else if (indexName.includes(prefix)) {\n // Extract the actual index name from chunk-prefixed names (e.g., _hyper_1_1_chunk___qd_xxx -> __qd_xxx)\n const actualName = indexName.substring(indexName.indexOf(prefix));\n newIndexes.add(actualName);\n } else {\n existingIndexes.add(indexName);\n }\n }\n });\n return {\n newIndexes,\n existingIndexes,\n };\n }\n\n private replaceUsedIndexesWithDefinition(\n explain: Record<string, any>,\n triedIndexes: Map<string, IndexRecommendation>,\n ) {\n walkExplain(explain, (stage) => {\n const indexName = stage[\"Index Name\"];\n if (typeof indexName === \"string\") {\n const recommendation = triedIndexes.get(indexName);\n if (recommendation) {\n stage[\"Index Name\"] = recommendation.definition;\n }\n }\n });\n }\n}\n\nfunction walkExplain(explain: Record<string, any>, f: (stage: any) => void) {\n function go(plan: any) {\n f(plan);\n if (plan.Plans) {\n for (const p of plan.Plans) {\n go(p);\n }\n }\n }\n go(explain);\n}\n\nexport type OptimizeResult =\n | {\n kind: \"ok\";\n baseExplainPlan: PostgresExplainStage;\n baseCost: number;\n finalCost: number;\n newIndexes: Set<string>;\n existingIndexes: Set<string>;\n triedIndexes: Map<string, IndexRecommendation>;\n explainPlan: PostgresExplainStage;\n }\n | {\n kind: \"zero_cost_plan\";\n explainPlan: PostgresExplainStage;\n };\n\nclass RollbackError<T> {\n constructor(public readonly value?: T) {}\n}\n\nexport type RootIndexCandidate = {\n schema: string;\n table: string;\n column: string;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n};\n\nexport type PermutedIndexCandidate = {\n schema: string;\n table: string;\n columns: RootIndexCandidate[];\n // TODO: functional indexes\n where?: string;\n indexMethod?: \"btree\" | \"gin\";\n opclass?: string;\n};\n\nexport const PROCEED = Symbol(\"PROCEED\");\nexport const SKIP = Symbol(\"SKIP\");\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
3
  const require_defineProperty = require("../_virtual/_@oxc-project_runtime@0.126.0/helpers/defineProperty.cjs");
4
+ const require_pg_identifier = require("../sql/pg-identifier.cjs");
4
5
  let colorette = require("colorette");
5
6
  let zod = require("zod");
6
7
  let dedent = require("dedent");
@@ -257,12 +258,16 @@ var Statistics = class Statistics {
257
258
  require_defineProperty._defineProperty(this, "mode", void 0);
258
259
  require_defineProperty._defineProperty(this, "computedStats", void 0);
259
260
  require_defineProperty._defineProperty(this, "exportedMetadata", void 0);
261
+ require_defineProperty._defineProperty(this, "additionalIndexes", []);
260
262
  if (statsMode) {
261
263
  this.mode = statsMode;
262
264
  if (statsMode.kind === "fromStatisticsExport") this.exportedMetadata = statsMode.stats;
263
265
  } else this.mode = Statistics.defaultStatsMode;
264
266
  this.computedStats = this.buildComputedStats();
265
267
  }
268
+ setAdditionalIndexes(additionalIndexes) {
269
+ this.additionalIndexes = additionalIndexes;
270
+ }
266
271
  buildComputedStats() {
267
272
  const columnStats = [];
268
273
  const reltuples = [];
@@ -458,6 +463,76 @@ var Statistics = class Statistics {
458
463
  }
459
464
  return values;
460
465
  }
466
+ /**
467
+ * When inserting fake stats for existing tables and indexes, we also need to
468
+ * account for data on newly created indexes by the optimizer.
469
+ *
470
+ * However we assume that index reltuples = table reltuples.
471
+ * Meaning this logic is going to be a little off for partial indexes
472
+ * or posting lists for duplicate values in btrees.
473
+ * Meaning the deduplication that happens in pg side
474
+ * 4 reltuples [(2, 1),(2, 9),(2, 4),(2, 8)] -> 1 reltuple [2, (1, 9, 4, 8)]
475
+ * does not get accounted for here.
476
+ */
477
+ async getAdditionalIndexReltuples(tx) {
478
+ if (this.additionalIndexes.length === 0) return [];
479
+ const indexNames = this.additionalIndexes.map((idx) => idx.name.toString());
480
+ const rows = await tx.exec(dedent.default`
481
+ SELECT
482
+ i.relname AS index_name,
483
+ n.nspname AS schema_name,
484
+ t.relname AS table_name,
485
+ a.attname AS column_name,
486
+ CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END AS attlen,
487
+ COALESCE(
488
+ (SELECT (regexp_match(opt, 'fillfactor=(\\d+)'))[1]::integer
489
+ FROM unnest(i.reloptions) AS opt
490
+ WHERE opt LIKE 'fillfactor=%'
491
+ LIMIT 1),
492
+ 90
493
+ ) AS fillfactor
494
+ FROM pg_class i
495
+ JOIN pg_index ix ON ix.indexrelid = i.oid
496
+ JOIN pg_class t ON t.oid = ix.indrelid
497
+ JOIN pg_namespace n ON n.oid = t.relnamespace
498
+ JOIN unnest(ix.indkey) AS k(attnum) ON true
499
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
500
+ WHERE i.relname IN (SELECT jsonb_array_elements_text($1::jsonb))
501
+ AND k.attnum > 0
502
+ `, [indexNames]).catch((err) => {
503
+ console.error("Something went wrong querying additional index column metadata");
504
+ console.error(err);
505
+ return [];
506
+ });
507
+ const attlenByKey = /* @__PURE__ */ new Map();
508
+ const fillfactorByIndex = /* @__PURE__ */ new Map();
509
+ for (const row of rows) {
510
+ attlenByKey.set(`${row.schema_name}.${row.table_name}.${row.column_name}`, row.attlen);
511
+ fillfactorByIndex.set(row.index_name, row.fillfactor);
512
+ }
513
+ return this.additionalIndexes.flatMap((idx) => {
514
+ const tableStats = this.computedStats.reltuples.find((r) => {
515
+ const sourceSchema = require_pg_identifier.PgIdentifier.fromString(r.schema_name).toString();
516
+ const sourceTable = require_pg_identifier.PgIdentifier.fromString(r.relname).toString();
517
+ const targetSchema = require_pg_identifier.PgIdentifier.fromString(idx.schema).toString();
518
+ const targetTable = require_pg_identifier.PgIdentifier.fromString(idx.table).toString();
519
+ return sourceSchema === targetSchema && sourceTable === targetTable;
520
+ });
521
+ if (!tableStats) return [];
522
+ const columns = idx.columns.map((col) => ({ attlen: attlenByKey.get(`${idx.schema}.${idx.table}.${col.column}`) ?? null }));
523
+ const fillfactor = fillfactorByIndex.get(idx.name.toString()) ?? 90;
524
+ const amname = idx.indexMethod ?? "btree";
525
+ const relpages = estimateIndexRelpages(tableStats.reltuples, columns, fillfactor / 100, amname, tableStats.relpages);
526
+ return [{
527
+ relname: idx.name.toString(),
528
+ schema_name: idx.schema,
529
+ reltuples: tableStats.reltuples,
530
+ relpages,
531
+ relallvisible: relpages,
532
+ relallfrozen: 0
533
+ }];
534
+ });
535
+ }
461
536
  async restoreStats17(tx) {
462
537
  const warnings = {
463
538
  tablesNotInExports: [],
@@ -508,7 +583,8 @@ var Statistics = class Statistics {
508
583
  and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)
509
584
  returning p.relname, p.relnamespace, p.reltuples, p.relpages;
510
585
  `;
511
- const reltuplesPromise = tx.exec(reltuplesQuery, [this.computedStats.reltuples]).catch((err) => {
586
+ const additionalIndexReltuples = await this.getAdditionalIndexReltuples(tx);
587
+ const reltuplesPromise = tx.exec(reltuplesQuery, [[...this.computedStats.reltuples, ...additionalIndexReltuples]]).catch((err) => {
512
588
  console.error("Something went wrong updating reltuples/relpages");
513
589
  console.error(err);
514
590
  return err;