@query-doctor/core 0.8.1-rc.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/optimizer/genalgo.cjs +13 -0
- package/dist/optimizer/genalgo.cjs.map +1 -1
- package/dist/optimizer/genalgo.d.cts.map +1 -1
- package/dist/optimizer/genalgo.d.mts.map +1 -1
- package/dist/optimizer/genalgo.mjs +13 -0
- package/dist/optimizer/genalgo.mjs.map +1 -1
- package/dist/sql/walker.cjs +3 -0
- package/dist/sql/walker.cjs.map +1 -1
- package/dist/sql/walker.d.cts.map +1 -1
- package/dist/sql/walker.d.mts.map +1 -1
- package/dist/sql/walker.mjs +3 -0
- package/dist/sql/walker.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -25,6 +25,19 @@ var IndexOptimizer = class IndexOptimizer {
|
|
|
25
25
|
explainPlan: baseExplain.Plan
|
|
26
26
|
};
|
|
27
27
|
const toCreate = this.indexesToCreate(indexes);
|
|
28
|
+
if (toCreate.length === 0) {
|
|
29
|
+
const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
|
|
30
|
+
return {
|
|
31
|
+
kind: "ok",
|
|
32
|
+
baseCost,
|
|
33
|
+
finalCost: baseCost,
|
|
34
|
+
newIndexes: /* @__PURE__ */ new Set(),
|
|
35
|
+
existingIndexes: baseIndexes.existingIndexes,
|
|
36
|
+
triedIndexes: /* @__PURE__ */ new Map(),
|
|
37
|
+
baseExplainPlan: baseExplain.Plan,
|
|
38
|
+
explainPlan: baseExplain.Plan
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
|
|
29
42
|
if (beforeQuery) await beforeQuery(tx);
|
|
30
43
|
for (const permutation of toCreate) {
|
|
@@ -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 { isIndexProbablyDroppable } from \"src/sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"src/sql/permutations.js\";\nimport { PgIdentifier } from \"src/sql/pg-identifier.js\";\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 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 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\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 +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;
|
|
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 +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;
|
|
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"}
|
|
@@ -24,6 +24,19 @@ var IndexOptimizer = class IndexOptimizer {
|
|
|
24
24
|
explainPlan: baseExplain.Plan
|
|
25
25
|
};
|
|
26
26
|
const toCreate = this.indexesToCreate(indexes);
|
|
27
|
+
if (toCreate.length === 0) {
|
|
28
|
+
const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
|
|
29
|
+
return {
|
|
30
|
+
kind: "ok",
|
|
31
|
+
baseCost,
|
|
32
|
+
finalCost: baseCost,
|
|
33
|
+
newIndexes: /* @__PURE__ */ new Set(),
|
|
34
|
+
existingIndexes: baseIndexes.existingIndexes,
|
|
35
|
+
triedIndexes: /* @__PURE__ */ new Map(),
|
|
36
|
+
baseExplainPlan: baseExplain.Plan,
|
|
37
|
+
explainPlan: baseExplain.Plan
|
|
38
|
+
};
|
|
39
|
+
}
|
|
27
40
|
const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
|
|
28
41
|
if (beforeQuery) await beforeQuery(tx);
|
|
29
42
|
for (const permutation of toCreate) {
|
|
@@ -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 { isIndexProbablyDroppable } from \"src/sql/indexes.js\";\nimport { permutationsWithDescendingLength } from \"src/sql/permutations.js\";\nimport { PgIdentifier } from \"src/sql/pg-identifier.js\";\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 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 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\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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"}
|
package/dist/sql/walker.cjs
CHANGED
|
@@ -62,6 +62,9 @@ var Walker = class Walker {
|
|
|
62
62
|
if (require_ast_utils.is(node, "UpdateStmt")) {
|
|
63
63
|
if (node.UpdateStmt.relation) this.registerTable(node.UpdateStmt.relation);
|
|
64
64
|
}
|
|
65
|
+
if (require_ast_utils.is(node, "DeleteStmt")) {
|
|
66
|
+
if (node.DeleteStmt.relation) this.registerTable(node.DeleteStmt.relation);
|
|
67
|
+
}
|
|
65
68
|
if (require_ast_utils.is(node, "RangeVar") && node.RangeVar.relname) this.registerTable(node.RangeVar);
|
|
66
69
|
if (require_ast_utils.is(node, "SortBy")) {
|
|
67
70
|
if (node.SortBy.node && require_ast_utils.is(node.SortBy.node, "ColumnRef")) this.add(node.SortBy.node, { sort: {
|
package/dist/sql/walker.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walker.cjs","names":["parseNudges","is","isANode","getNodeKind"],"sources":["../../src/sql/walker.ts"],"sourcesContent":["import type { Node, NullTestType, RangeVar } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport type {\n DiscoveredColumnReference,\n JsonbOperator,\n SortContext,\n} from \"./analyzer.js\";\nimport { getNodeKind, is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { parseNudges } from \"./nudges.js\";\nimport type { Nudge } from \"./nudges.js\";\n\nconst JSONB_EXTRACTION_OPS = new Set([\"->\", \"->>\"]);\nconst COMPARISON_OPS = new Set([\n \"=\",\n \"<>\",\n \"!=\",\n \"<\",\n \"<=\",\n \">\",\n \">=\",\n \"~~\",\n \"~~*\",\n \"!~~\",\n \"!~~*\",\n]);\n\n/** Information about tables that appear in the query */\nexport type TableMappings = Map<string, ColumnReferencePart>;\ntype SeenReferences = Map<string, number>;\n\n/**\n * Walks the AST of a sql query and extracts query metadata.\n * This pattern is used to segregate the mutable state that's more common for the\n * AST walking process from the rest of the analyzer.\n */\nexport class Walker {\n private tableMappings: TableMappings = new Map();\n private tempTables = new Set<string>();\n private highlights: DiscoveredColumnReference[] = [];\n private indexRepresentations = new Set<string>();\n private indexesToCheck: DiscoveredColumnReference[] = [];\n private highlightPositions = new Set<number>();\n // used for tallying the amount of times we see stuff so\n // we have a better idea of what to start off the algorithm with\n private seenReferences: SeenReferences = new Map();\n private shadowedAliases: ColumnReferencePart[] = [];\n private nudges: Nudge[] = [];\n\n constructor(private readonly query: string) {}\n\n walk(root: Node) {\n // reset state in case the class instance is reused\n // reassigning vars here instead of using `map.clear()` to prevent\n // accidentally mutating existing references\n this.tableMappings = new Map();\n this.tempTables = new Set<string>();\n this.highlights = [];\n this.indexRepresentations = new Set<string>();\n this.indexesToCheck = [];\n this.highlightPositions = new Set<number>();\n this.seenReferences = new Map();\n this.shadowedAliases = [];\n this.nudges = [];\n\n Walker.traverse(root, (node, stack) => {\n const nodeNudges = parseNudges(node, stack);\n this.nudges = [...this.nudges, ...nodeNudges];\n\n // comments are not parsed here as they seem to be ignored.\n //\n // results cannot be indexed in any way because they alias a CTE\n // with alias as (select ...)\n // ^^^^^\n if (is(node, \"CommonTableExpr\")) {\n if (node.CommonTableExpr.ctename) {\n this.tempTables.add(node.CommonTableExpr.ctename);\n }\n }\n // results cannot be indexed in any way because they alias a subquery\n // select ... from (...) as alias\n // ^^^^^\n if (is(node, \"RangeSubselect\")) {\n if (node.RangeSubselect.alias?.aliasname) {\n this.tempTables.add(node.RangeSubselect.alias.aliasname);\n }\n }\n // select ... from (...) where col is null\n // ^^^^^^^\n if (is(node, \"NullTest\")) {\n if (\n node.NullTest.arg &&\n node.NullTest.nulltesttype &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n this.add(node.NullTest.arg, {\n where: { nulltest: node.NullTest.nulltesttype },\n });\n }\n }\n // The parser does not wrap the `relation` field of UpdateStmt\n // in a { RangeVar: ... } node (it's a raw RangeVar struct), so the\n // generic RangeVar handler below never sees it. Handle it explicitly.\n if (is(node, \"UpdateStmt\")) {\n if (node.UpdateStmt.relation) {\n this.registerTable(node.UpdateStmt.relation);\n }\n }\n // can be indexed as the alias refers to a regular table\n // but the alias has to be mapped to the original table name\n // select ... from table as alias\n // ^^^^^\n if (is(node, \"RangeVar\") && node.RangeVar.relname) {\n this.registerTable(node.RangeVar);\n }\n // select ... from table order by col asc\n // ^^^^^^^^^^^^^^^^\n if (is(node, \"SortBy\")) {\n // we don't care about sorting by anything that's not a column reference\n // because it couldn't be indexed anyway.\n // TODO: mark that expression as unindexable? It's just better for debugging\n if (node.SortBy.node && is(node.SortBy.node, \"ColumnRef\")) {\n this.add(node.SortBy.node, {\n sort: {\n dir: node.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\",\n nulls: node.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\",\n },\n });\n }\n }\n // select ... from table1 join table2 t2 on table1.col = t2.col\n // ^^\n if (is(node, \"JoinExpr\") && node.JoinExpr.quals) {\n if (is(node.JoinExpr.quals, \"A_Expr\")) {\n if (\n node.JoinExpr.quals.A_Expr.lexpr &&\n is(node.JoinExpr.quals.A_Expr.lexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.lexpr);\n }\n if (\n node.JoinExpr.quals.A_Expr.rexpr &&\n is(node.JoinExpr.quals.A_Expr.rexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.rexpr);\n }\n }\n }\n // select ... from table where data @> '{\"key\": \"val\"}'\n // ^^^^\n if (is(node, \"A_Expr\") && node.A_Expr.kind === \"AEXPR_OP\") {\n const opName =\n node.A_Expr.name?.[0] &&\n is(node.A_Expr.name[0], \"String\") &&\n node.A_Expr.name[0].String.sval;\n if (\n opName &&\n (opName === \"@>\" ||\n opName === \"?\" ||\n opName === \"?|\" ||\n opName === \"?&\" ||\n opName === \"@@\" ||\n opName === \"@?\")\n ) {\n const jsonbOperator = opName as JsonbOperator;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.lexpr, { jsonbOperator });\n }\n if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.rexpr, { jsonbOperator });\n }\n }\n // select ... from table where data->>'email' = 'alice@example.com'\n // ^^^^^^^^^^^^^^\n if (opName && COMPARISON_OPS.has(opName)) {\n for (const operand of [node.A_Expr.lexpr, node.A_Expr.rexpr]) {\n if (!operand) continue;\n const extraction = extractJsonbPath(operand);\n if (extraction) {\n this.add(extraction.columnRef, {\n jsonbExtraction: extraction.expression,\n });\n }\n }\n }\n }\n // any column reference anywhere\n if (is(node, \"ColumnRef\")) {\n // TODO: this approach needs refinement\n for (let i = 0; i < stack.length; i++) {\n const inReturningList =\n stack[i] === \"returningList\" &&\n stack[i + 1] === \"ResTarget\" &&\n stack[i + 2] === \"val\" &&\n stack[i + 3] === \"ColumnRef\";\n if (inReturningList) {\n this.add(node, { ignored: true });\n return;\n }\n if (\n // stack[i] === \"SelectStmt\" &&\n stack[i + 1] === \"targetList\" &&\n stack[i + 2] === \"ResTarget\" &&\n stack[i + 3] === \"val\" &&\n stack[i + 4] === \"ColumnRef\"\n ) {\n // we don't want to index the columns that are being selected\n this.add(node, { ignored: true });\n return;\n }\n\n // TODO: add functional index support here\n if (stack[i] === \"FuncCall\" && stack[i + 1] === \"args\") {\n // args of a function call can't be indexed (without functional indexes)\n this.add(node, { ignored: true });\n return;\n }\n }\n this.add(node);\n }\n });\n\n return {\n highlights: this.highlights,\n indexRepresentations: this.indexRepresentations,\n indexesToCheck: this.indexesToCheck,\n shadowedAliases: this.shadowedAliases,\n tempTables: this.tempTables,\n tableMappings: this.tableMappings,\n nudges: this.nudges,\n };\n }\n\n private add(\n node: Extract<Node, { ColumnRef: unknown }>,\n options?: {\n ignored?: boolean;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n },\n ) {\n if (!node.ColumnRef.location) {\n console.error(`Node did not have a location. Skipping`, node);\n return;\n }\n if (!node.ColumnRef.fields) {\n console.error(node);\n throw new Error(\"Column reference must have fields\");\n }\n let ignored = options?.ignored ?? false;\n let runningLength: number = node.ColumnRef.location;\n const parts: ColumnReferencePart[] = node.ColumnRef.fields.map(\n (field, i, length) => {\n if (!is(field, \"String\") || !field.String.sval) {\n const out = deparseSync(field);\n ignored = true;\n return {\n quoted: out.startsWith('\"'),\n text: out,\n start: runningLength,\n };\n }\n const start = runningLength;\n const size = field.String.sval?.length ?? 0;\n let quoted = false;\n if (node.ColumnRef.location !== undefined) {\n const boundary = this.query[runningLength];\n if (boundary === '\"') {\n quoted = true;\n }\n }\n // +1 for the dot that comes after\n const isLastIteration = i === length.length - 1;\n runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);\n return {\n text: field.String.sval,\n start,\n quoted,\n };\n },\n );\n const end = runningLength;\n if (this.highlightPositions.has(node.ColumnRef.location)) {\n return;\n }\n this.highlightPositions.add(node.ColumnRef.location);\n const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;\n const seen = this.seenReferences.get(highlighted);\n if (!ignored) {\n this.seenReferences.set(highlighted, (seen ?? 0) + 1);\n }\n const ref: DiscoveredColumnReference = {\n frequency: seen ?? 1,\n representation: highlighted,\n parts,\n ignored: ignored ?? false,\n position: {\n start: node.ColumnRef.location,\n end,\n },\n };\n if (options?.sort) {\n ref.sort = options.sort;\n }\n if (options?.where) {\n ref.where = options.where;\n }\n if (options?.jsonbOperator) {\n ref.jsonbOperator = options.jsonbOperator;\n }\n if (options?.jsonbExtraction) {\n ref.jsonbExtraction = options.jsonbExtraction;\n }\n this.highlights.push(ref);\n }\n\n private registerTable(rangeVar: RangeVar) {\n if (!rangeVar.relname) return;\n\n const columnReference: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: false,\n };\n if (rangeVar.schemaname) {\n columnReference.schema = rangeVar.schemaname;\n }\n this.tableMappings.set(rangeVar.relname, columnReference);\n\n if (rangeVar.alias?.aliasname) {\n const aliasName = rangeVar.alias.aliasname;\n const existingMapping = this.tableMappings.get(aliasName);\n const part: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: true,\n alias: aliasName,\n };\n if (rangeVar.schemaname) {\n part.schema = rangeVar.schemaname;\n }\n if (existingMapping) {\n const isSystemCatalog = rangeVar.relname?.startsWith(\"pg_\") ?? false;\n if (!isSystemCatalog) {\n console.warn(\n `Ignoring alias ${aliasName} as it shadows an existing mapping for ${existingMapping.text}. We currently do not support alias shadowing.`,\n );\n }\n this.shadowedAliases.push(part);\n return;\n }\n this.tableMappings.set(aliasName, part);\n }\n }\n\n /**\n * Descend only into shallow combinators of a node such as\n * - And\n * - Or\n * - Not\n * - ::typecast\n * without deep traversing into subqueries. Useful for checking members\n * of a `WHERE` clause\n */\n static shallowMatch<K extends KeysOfUnion<Node>>(\n expr: Node,\n kind: K,\n callback: (node: Extract<Node, Record<K, unknown>>) => void,\n ) {\n if (is(expr, kind)) {\n callback(expr as Extract<Node, Record<K, unknown>>);\n return;\n }\n if (is(expr, \"BoolExpr\") && expr.BoolExpr.args) {\n for (const arg of expr.BoolExpr.args) {\n Walker.shallowMatch(arg, kind, callback);\n }\n return;\n }\n if (is(expr, \"A_Expr\")) {\n if (expr.A_Expr.lexpr)\n Walker.shallowMatch(expr.A_Expr.lexpr, kind, callback);\n if (expr.A_Expr.rexpr)\n Walker.shallowMatch(expr.A_Expr.rexpr, kind, callback);\n return;\n }\n if (is(expr, \"NullTest\") && expr.NullTest.arg) {\n Walker.shallowMatch(expr.NullTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"BooleanTest\") && expr.BooleanTest.arg) {\n Walker.shallowMatch(expr.BooleanTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"SubLink\") && expr.SubLink.testexpr) {\n Walker.shallowMatch(expr.SubLink.testexpr, kind, callback);\n return;\n }\n if (is(expr, \"TypeCast\") && expr.TypeCast.arg) {\n Walker.shallowMatch(expr.TypeCast.arg, kind, callback);\n return;\n }\n }\n\n static traverse(\n node: Node,\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n Walker.doTraverse(node, [], callback);\n }\n\n private static doTraverse(\n node: unknown,\n stack: (KeysOfUnion<Node> | string)[],\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n if (isANode(node)) {\n callback(node, [...stack, getNodeKind(node)]);\n }\n if (typeof node !== \"object\" || node === null) {\n return;\n }\n if (Array.isArray(node)) {\n for (const item of node) {\n if (isANode(item)) {\n Walker.doTraverse(item, stack, callback);\n }\n }\n } else if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);\n } else {\n for (const [key, child] of Object.entries(node)) {\n Walker.doTraverse(\n child,\n [...stack, key as KeysOfUnion<Node>],\n callback,\n );\n }\n }\n }\n}\n\nexport type ColumnReferencePart = {\n schema?: string;\n /** the text of the column reference (excluding any potential quotes) */\n text: string;\n start?: number;\n quoted: boolean;\n alias?: string;\n};\n\ntype JsonbExtraction = {\n columnRef: Extract<Node, { ColumnRef: unknown }>;\n expression: string;\n};\n\n/**\n * Given an operand of a comparison (e.g. the left side of `=`), check whether\n * it is a JSONB path extraction expression such as `data->>'email'` or\n * `(data->>'age')::int`. If so, return the root ColumnRef (for the walker to\n * register) and the full expression string (for use in the expression index).\n *\n * Handles:\n * - `data->>'email'` — simple extraction\n * - `(data->>'age')::int` — extraction with cast\n * - `data->'addr'->>'city'` — chained extraction\n * - `t.data->>'email'` — table-qualified (strips qualifier from expression)\n */\nfunction extractJsonbPath(node: Node): JsonbExtraction | undefined {\n let exprNode = node;\n // Unwrap TypeCast (e.g. `(data->>'age')::int`)\n if (is(exprNode, \"TypeCast\") && exprNode.TypeCast.arg) {\n exprNode = exprNode.TypeCast.arg;\n }\n if (!is(exprNode, \"A_Expr\") || exprNode.A_Expr.kind !== \"AEXPR_OP\") {\n return undefined;\n }\n const innerOp =\n exprNode.A_Expr.name?.[0] &&\n is(exprNode.A_Expr.name[0], \"String\") &&\n exprNode.A_Expr.name[0].String.sval;\n if (!innerOp || !JSONB_EXTRACTION_OPS.has(innerOp)) {\n return undefined;\n }\n const rootCol = findRootColumnRef(exprNode);\n if (!rootCol) {\n return undefined;\n }\n // Deep-clone the whole outer node so we can strip qualifiers without\n // mutating the original AST.\n const cloned: Node = JSON.parse(JSON.stringify(node));\n stripTableQualifiers(cloned);\n const expression = deparseSync(cloned);\n return { columnRef: rootCol, expression };\n}\n\n/**\n * Walk the left side of a chain of `->` / `->>` operators to find the\n * root ColumnRef (the JSONB column itself).\n */\nfunction findRootColumnRef(\n node: Node,\n): Extract<Node, { ColumnRef: unknown }> | undefined {\n if (is(node, \"ColumnRef\")) {\n return node;\n }\n if (is(node, \"A_Expr\") && node.A_Expr.lexpr) {\n return findRootColumnRef(node.A_Expr.lexpr);\n }\n if (is(node, \"TypeCast\") && node.TypeCast.arg) {\n return findRootColumnRef(node.TypeCast.arg);\n }\n return undefined;\n}\n\n/**\n * Remove table/schema qualifiers from ColumnRef nodes inside the\n * expression so the deparsed expression contains only the column name.\n * e.g. `t.data->>'email'` → `data->>'email'`\n */\nfunction stripTableQualifiers(node: unknown): void {\n if (typeof node !== \"object\" || node === null) return;\n if (\n \"ColumnRef\" in (node as Record<string, unknown>) &&\n (node as any).ColumnRef?.fields\n ) {\n const fields: unknown[] = (node as any).ColumnRef.fields;\n if (fields.length > 1) {\n (node as any).ColumnRef.fields = [fields[fields.length - 1]];\n }\n return;\n }\n for (const value of Object.values(node as Record<string, unknown>)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n stripTableQualifiers(item);\n }\n } else if (typeof value === \"object\" && value !== null) {\n stripTableQualifiers(value);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"walker.cjs","names":["parseNudges","is","isANode","getNodeKind"],"sources":["../../src/sql/walker.ts"],"sourcesContent":["import type { Node, NullTestType, RangeVar } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport type {\n DiscoveredColumnReference,\n JsonbOperator,\n SortContext,\n} from \"./analyzer.js\";\nimport { getNodeKind, is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { parseNudges } from \"./nudges.js\";\nimport type { Nudge } from \"./nudges.js\";\n\nconst JSONB_EXTRACTION_OPS = new Set([\"->\", \"->>\"]);\nconst COMPARISON_OPS = new Set([\n \"=\",\n \"<>\",\n \"!=\",\n \"<\",\n \"<=\",\n \">\",\n \">=\",\n \"~~\",\n \"~~*\",\n \"!~~\",\n \"!~~*\",\n]);\n\n/** Information about tables that appear in the query */\nexport type TableMappings = Map<string, ColumnReferencePart>;\ntype SeenReferences = Map<string, number>;\n\n/**\n * Walks the AST of a sql query and extracts query metadata.\n * This pattern is used to segregate the mutable state that's more common for the\n * AST walking process from the rest of the analyzer.\n */\nexport class Walker {\n private tableMappings: TableMappings = new Map();\n private tempTables = new Set<string>();\n private highlights: DiscoveredColumnReference[] = [];\n private indexRepresentations = new Set<string>();\n private indexesToCheck: DiscoveredColumnReference[] = [];\n private highlightPositions = new Set<number>();\n // used for tallying the amount of times we see stuff so\n // we have a better idea of what to start off the algorithm with\n private seenReferences: SeenReferences = new Map();\n private shadowedAliases: ColumnReferencePart[] = [];\n private nudges: Nudge[] = [];\n\n constructor(private readonly query: string) {}\n\n walk(root: Node) {\n // reset state in case the class instance is reused\n // reassigning vars here instead of using `map.clear()` to prevent\n // accidentally mutating existing references\n this.tableMappings = new Map();\n this.tempTables = new Set<string>();\n this.highlights = [];\n this.indexRepresentations = new Set<string>();\n this.indexesToCheck = [];\n this.highlightPositions = new Set<number>();\n this.seenReferences = new Map();\n this.shadowedAliases = [];\n this.nudges = [];\n\n Walker.traverse(root, (node, stack) => {\n const nodeNudges = parseNudges(node, stack);\n this.nudges = [...this.nudges, ...nodeNudges];\n\n // comments are not parsed here as they seem to be ignored.\n //\n // results cannot be indexed in any way because they alias a CTE\n // with alias as (select ...)\n // ^^^^^\n if (is(node, \"CommonTableExpr\")) {\n if (node.CommonTableExpr.ctename) {\n this.tempTables.add(node.CommonTableExpr.ctename);\n }\n }\n // results cannot be indexed in any way because they alias a subquery\n // select ... from (...) as alias\n // ^^^^^\n if (is(node, \"RangeSubselect\")) {\n if (node.RangeSubselect.alias?.aliasname) {\n this.tempTables.add(node.RangeSubselect.alias.aliasname);\n }\n }\n // select ... from (...) where col is null\n // ^^^^^^^\n if (is(node, \"NullTest\")) {\n if (\n node.NullTest.arg &&\n node.NullTest.nulltesttype &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n this.add(node.NullTest.arg, {\n where: { nulltest: node.NullTest.nulltesttype },\n });\n }\n }\n // The parser does not wrap the `relation` field of UpdateStmt\n // in a { RangeVar: ... } node (it's a raw RangeVar struct), so the\n // generic RangeVar handler below never sees it. Handle it explicitly.\n if (is(node, \"UpdateStmt\")) {\n if (node.UpdateStmt.relation) {\n this.registerTable(node.UpdateStmt.relation);\n }\n }\n // Same as UpdateStmt: DeleteStmt.relation is a raw RangeVar struct.\n if (is(node, \"DeleteStmt\")) {\n if (node.DeleteStmt.relation) {\n this.registerTable(node.DeleteStmt.relation);\n }\n }\n // can be indexed as the alias refers to a regular table\n // but the alias has to be mapped to the original table name\n // select ... from table as alias\n // ^^^^^\n if (is(node, \"RangeVar\") && node.RangeVar.relname) {\n this.registerTable(node.RangeVar);\n }\n // select ... from table order by col asc\n // ^^^^^^^^^^^^^^^^\n if (is(node, \"SortBy\")) {\n // we don't care about sorting by anything that's not a column reference\n // because it couldn't be indexed anyway.\n // TODO: mark that expression as unindexable? It's just better for debugging\n if (node.SortBy.node && is(node.SortBy.node, \"ColumnRef\")) {\n this.add(node.SortBy.node, {\n sort: {\n dir: node.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\",\n nulls: node.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\",\n },\n });\n }\n }\n // select ... from table1 join table2 t2 on table1.col = t2.col\n // ^^\n if (is(node, \"JoinExpr\") && node.JoinExpr.quals) {\n if (is(node.JoinExpr.quals, \"A_Expr\")) {\n if (\n node.JoinExpr.quals.A_Expr.lexpr &&\n is(node.JoinExpr.quals.A_Expr.lexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.lexpr);\n }\n if (\n node.JoinExpr.quals.A_Expr.rexpr &&\n is(node.JoinExpr.quals.A_Expr.rexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.rexpr);\n }\n }\n }\n // select ... from table where data @> '{\"key\": \"val\"}'\n // ^^^^\n if (is(node, \"A_Expr\") && node.A_Expr.kind === \"AEXPR_OP\") {\n const opName =\n node.A_Expr.name?.[0] &&\n is(node.A_Expr.name[0], \"String\") &&\n node.A_Expr.name[0].String.sval;\n if (\n opName &&\n (opName === \"@>\" ||\n opName === \"?\" ||\n opName === \"?|\" ||\n opName === \"?&\" ||\n opName === \"@@\" ||\n opName === \"@?\")\n ) {\n const jsonbOperator = opName as JsonbOperator;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.lexpr, { jsonbOperator });\n }\n if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.rexpr, { jsonbOperator });\n }\n }\n // select ... from table where data->>'email' = 'alice@example.com'\n // ^^^^^^^^^^^^^^\n if (opName && COMPARISON_OPS.has(opName)) {\n for (const operand of [node.A_Expr.lexpr, node.A_Expr.rexpr]) {\n if (!operand) continue;\n const extraction = extractJsonbPath(operand);\n if (extraction) {\n this.add(extraction.columnRef, {\n jsonbExtraction: extraction.expression,\n });\n }\n }\n }\n }\n // any column reference anywhere\n if (is(node, \"ColumnRef\")) {\n // TODO: this approach needs refinement\n for (let i = 0; i < stack.length; i++) {\n const inReturningList =\n stack[i] === \"returningList\" &&\n stack[i + 1] === \"ResTarget\" &&\n stack[i + 2] === \"val\" &&\n stack[i + 3] === \"ColumnRef\";\n if (inReturningList) {\n this.add(node, { ignored: true });\n return;\n }\n if (\n // stack[i] === \"SelectStmt\" &&\n stack[i + 1] === \"targetList\" &&\n stack[i + 2] === \"ResTarget\" &&\n stack[i + 3] === \"val\" &&\n stack[i + 4] === \"ColumnRef\"\n ) {\n // we don't want to index the columns that are being selected\n this.add(node, { ignored: true });\n return;\n }\n\n // TODO: add functional index support here\n if (stack[i] === \"FuncCall\" && stack[i + 1] === \"args\") {\n // args of a function call can't be indexed (without functional indexes)\n this.add(node, { ignored: true });\n return;\n }\n }\n this.add(node);\n }\n });\n\n return {\n highlights: this.highlights,\n indexRepresentations: this.indexRepresentations,\n indexesToCheck: this.indexesToCheck,\n shadowedAliases: this.shadowedAliases,\n tempTables: this.tempTables,\n tableMappings: this.tableMappings,\n nudges: this.nudges,\n };\n }\n\n private add(\n node: Extract<Node, { ColumnRef: unknown }>,\n options?: {\n ignored?: boolean;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n },\n ) {\n if (!node.ColumnRef.location) {\n console.error(`Node did not have a location. Skipping`, node);\n return;\n }\n if (!node.ColumnRef.fields) {\n console.error(node);\n throw new Error(\"Column reference must have fields\");\n }\n let ignored = options?.ignored ?? false;\n let runningLength: number = node.ColumnRef.location;\n const parts: ColumnReferencePart[] = node.ColumnRef.fields.map(\n (field, i, length) => {\n if (!is(field, \"String\") || !field.String.sval) {\n const out = deparseSync(field);\n ignored = true;\n return {\n quoted: out.startsWith('\"'),\n text: out,\n start: runningLength,\n };\n }\n const start = runningLength;\n const size = field.String.sval?.length ?? 0;\n let quoted = false;\n if (node.ColumnRef.location !== undefined) {\n const boundary = this.query[runningLength];\n if (boundary === '\"') {\n quoted = true;\n }\n }\n // +1 for the dot that comes after\n const isLastIteration = i === length.length - 1;\n runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);\n return {\n text: field.String.sval,\n start,\n quoted,\n };\n },\n );\n const end = runningLength;\n if (this.highlightPositions.has(node.ColumnRef.location)) {\n return;\n }\n this.highlightPositions.add(node.ColumnRef.location);\n const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;\n const seen = this.seenReferences.get(highlighted);\n if (!ignored) {\n this.seenReferences.set(highlighted, (seen ?? 0) + 1);\n }\n const ref: DiscoveredColumnReference = {\n frequency: seen ?? 1,\n representation: highlighted,\n parts,\n ignored: ignored ?? false,\n position: {\n start: node.ColumnRef.location,\n end,\n },\n };\n if (options?.sort) {\n ref.sort = options.sort;\n }\n if (options?.where) {\n ref.where = options.where;\n }\n if (options?.jsonbOperator) {\n ref.jsonbOperator = options.jsonbOperator;\n }\n if (options?.jsonbExtraction) {\n ref.jsonbExtraction = options.jsonbExtraction;\n }\n this.highlights.push(ref);\n }\n\n private registerTable(rangeVar: RangeVar) {\n if (!rangeVar.relname) return;\n\n const columnReference: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: false,\n };\n if (rangeVar.schemaname) {\n columnReference.schema = rangeVar.schemaname;\n }\n this.tableMappings.set(rangeVar.relname, columnReference);\n\n if (rangeVar.alias?.aliasname) {\n const aliasName = rangeVar.alias.aliasname;\n const existingMapping = this.tableMappings.get(aliasName);\n const part: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: true,\n alias: aliasName,\n };\n if (rangeVar.schemaname) {\n part.schema = rangeVar.schemaname;\n }\n if (existingMapping) {\n const isSystemCatalog = rangeVar.relname?.startsWith(\"pg_\") ?? false;\n if (!isSystemCatalog) {\n console.warn(\n `Ignoring alias ${aliasName} as it shadows an existing mapping for ${existingMapping.text}. We currently do not support alias shadowing.`,\n );\n }\n this.shadowedAliases.push(part);\n return;\n }\n this.tableMappings.set(aliasName, part);\n }\n }\n\n /**\n * Descend only into shallow combinators of a node such as\n * - And\n * - Or\n * - Not\n * - ::typecast\n * without deep traversing into subqueries. Useful for checking members\n * of a `WHERE` clause\n */\n static shallowMatch<K extends KeysOfUnion<Node>>(\n expr: Node,\n kind: K,\n callback: (node: Extract<Node, Record<K, unknown>>) => void,\n ) {\n if (is(expr, kind)) {\n callback(expr as Extract<Node, Record<K, unknown>>);\n return;\n }\n if (is(expr, \"BoolExpr\") && expr.BoolExpr.args) {\n for (const arg of expr.BoolExpr.args) {\n Walker.shallowMatch(arg, kind, callback);\n }\n return;\n }\n if (is(expr, \"A_Expr\")) {\n if (expr.A_Expr.lexpr)\n Walker.shallowMatch(expr.A_Expr.lexpr, kind, callback);\n if (expr.A_Expr.rexpr)\n Walker.shallowMatch(expr.A_Expr.rexpr, kind, callback);\n return;\n }\n if (is(expr, \"NullTest\") && expr.NullTest.arg) {\n Walker.shallowMatch(expr.NullTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"BooleanTest\") && expr.BooleanTest.arg) {\n Walker.shallowMatch(expr.BooleanTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"SubLink\") && expr.SubLink.testexpr) {\n Walker.shallowMatch(expr.SubLink.testexpr, kind, callback);\n return;\n }\n if (is(expr, \"TypeCast\") && expr.TypeCast.arg) {\n Walker.shallowMatch(expr.TypeCast.arg, kind, callback);\n return;\n }\n }\n\n static traverse(\n node: Node,\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n Walker.doTraverse(node, [], callback);\n }\n\n private static doTraverse(\n node: unknown,\n stack: (KeysOfUnion<Node> | string)[],\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n if (isANode(node)) {\n callback(node, [...stack, getNodeKind(node)]);\n }\n if (typeof node !== \"object\" || node === null) {\n return;\n }\n if (Array.isArray(node)) {\n for (const item of node) {\n if (isANode(item)) {\n Walker.doTraverse(item, stack, callback);\n }\n }\n } else if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);\n } else {\n for (const [key, child] of Object.entries(node)) {\n Walker.doTraverse(\n child,\n [...stack, key as KeysOfUnion<Node>],\n callback,\n );\n }\n }\n }\n}\n\nexport type ColumnReferencePart = {\n schema?: string;\n /** the text of the column reference (excluding any potential quotes) */\n text: string;\n start?: number;\n quoted: boolean;\n alias?: string;\n};\n\ntype JsonbExtraction = {\n columnRef: Extract<Node, { ColumnRef: unknown }>;\n expression: string;\n};\n\n/**\n * Given an operand of a comparison (e.g. the left side of `=`), check whether\n * it is a JSONB path extraction expression such as `data->>'email'` or\n * `(data->>'age')::int`. If so, return the root ColumnRef (for the walker to\n * register) and the full expression string (for use in the expression index).\n *\n * Handles:\n * - `data->>'email'` — simple extraction\n * - `(data->>'age')::int` — extraction with cast\n * - `data->'addr'->>'city'` — chained extraction\n * - `t.data->>'email'` — table-qualified (strips qualifier from expression)\n */\nfunction extractJsonbPath(node: Node): JsonbExtraction | undefined {\n let exprNode = node;\n // Unwrap TypeCast (e.g. `(data->>'age')::int`)\n if (is(exprNode, \"TypeCast\") && exprNode.TypeCast.arg) {\n exprNode = exprNode.TypeCast.arg;\n }\n if (!is(exprNode, \"A_Expr\") || exprNode.A_Expr.kind !== \"AEXPR_OP\") {\n return undefined;\n }\n const innerOp =\n exprNode.A_Expr.name?.[0] &&\n is(exprNode.A_Expr.name[0], \"String\") &&\n exprNode.A_Expr.name[0].String.sval;\n if (!innerOp || !JSONB_EXTRACTION_OPS.has(innerOp)) {\n return undefined;\n }\n const rootCol = findRootColumnRef(exprNode);\n if (!rootCol) {\n return undefined;\n }\n // Deep-clone the whole outer node so we can strip qualifiers without\n // mutating the original AST.\n const cloned: Node = JSON.parse(JSON.stringify(node));\n stripTableQualifiers(cloned);\n const expression = deparseSync(cloned);\n return { columnRef: rootCol, expression };\n}\n\n/**\n * Walk the left side of a chain of `->` / `->>` operators to find the\n * root ColumnRef (the JSONB column itself).\n */\nfunction findRootColumnRef(\n node: Node,\n): Extract<Node, { ColumnRef: unknown }> | undefined {\n if (is(node, \"ColumnRef\")) {\n return node;\n }\n if (is(node, \"A_Expr\") && node.A_Expr.lexpr) {\n return findRootColumnRef(node.A_Expr.lexpr);\n }\n if (is(node, \"TypeCast\") && node.TypeCast.arg) {\n return findRootColumnRef(node.TypeCast.arg);\n }\n return undefined;\n}\n\n/**\n * Remove table/schema qualifiers from ColumnRef nodes inside the\n * expression so the deparsed expression contains only the column name.\n * e.g. `t.data->>'email'` → `data->>'email'`\n */\nfunction stripTableQualifiers(node: unknown): void {\n if (typeof node !== \"object\" || node === null) return;\n if (\n \"ColumnRef\" in (node as Record<string, unknown>) &&\n (node as any).ColumnRef?.fields\n ) {\n const fields: unknown[] = (node as any).ColumnRef.fields;\n if (fields.length > 1) {\n (node as any).ColumnRef.fields = [fields[fields.length - 1]];\n }\n return;\n }\n for (const value of Object.values(node as Record<string, unknown>)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n stripTableQualifiers(item);\n }\n } else if (typeof value === \"object\" && value !== null) {\n stripTableQualifiers(value);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walker.d.cts","names":[],"sources":["../../src/sql/walker.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"walker.d.cts","names":[],"sources":["../../src/sql/walker.ts"],"mappings":";;;;;KAmcY,mBAAA;EACV,MAAA;EAEA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walker.d.mts","names":[],"sources":["../../src/sql/walker.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"walker.d.mts","names":[],"sources":["../../src/sql/walker.ts"],"mappings":";;;;;KAmcY,mBAAA;EACV,MAAA;EAEA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;AAAA"}
|
package/dist/sql/walker.mjs
CHANGED
|
@@ -61,6 +61,9 @@ var Walker = class Walker {
|
|
|
61
61
|
if (is(node, "UpdateStmt")) {
|
|
62
62
|
if (node.UpdateStmt.relation) this.registerTable(node.UpdateStmt.relation);
|
|
63
63
|
}
|
|
64
|
+
if (is(node, "DeleteStmt")) {
|
|
65
|
+
if (node.DeleteStmt.relation) this.registerTable(node.DeleteStmt.relation);
|
|
66
|
+
}
|
|
64
67
|
if (is(node, "RangeVar") && node.RangeVar.relname) this.registerTable(node.RangeVar);
|
|
65
68
|
if (is(node, "SortBy")) {
|
|
66
69
|
if (node.SortBy.node && is(node.SortBy.node, "ColumnRef")) this.add(node.SortBy.node, { sort: {
|
package/dist/sql/walker.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walker.mjs","names":[],"sources":["../../src/sql/walker.ts"],"sourcesContent":["import type { Node, NullTestType, RangeVar } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport type {\n DiscoveredColumnReference,\n JsonbOperator,\n SortContext,\n} from \"./analyzer.js\";\nimport { getNodeKind, is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { parseNudges } from \"./nudges.js\";\nimport type { Nudge } from \"./nudges.js\";\n\nconst JSONB_EXTRACTION_OPS = new Set([\"->\", \"->>\"]);\nconst COMPARISON_OPS = new Set([\n \"=\",\n \"<>\",\n \"!=\",\n \"<\",\n \"<=\",\n \">\",\n \">=\",\n \"~~\",\n \"~~*\",\n \"!~~\",\n \"!~~*\",\n]);\n\n/** Information about tables that appear in the query */\nexport type TableMappings = Map<string, ColumnReferencePart>;\ntype SeenReferences = Map<string, number>;\n\n/**\n * Walks the AST of a sql query and extracts query metadata.\n * This pattern is used to segregate the mutable state that's more common for the\n * AST walking process from the rest of the analyzer.\n */\nexport class Walker {\n private tableMappings: TableMappings = new Map();\n private tempTables = new Set<string>();\n private highlights: DiscoveredColumnReference[] = [];\n private indexRepresentations = new Set<string>();\n private indexesToCheck: DiscoveredColumnReference[] = [];\n private highlightPositions = new Set<number>();\n // used for tallying the amount of times we see stuff so\n // we have a better idea of what to start off the algorithm with\n private seenReferences: SeenReferences = new Map();\n private shadowedAliases: ColumnReferencePart[] = [];\n private nudges: Nudge[] = [];\n\n constructor(private readonly query: string) {}\n\n walk(root: Node) {\n // reset state in case the class instance is reused\n // reassigning vars here instead of using `map.clear()` to prevent\n // accidentally mutating existing references\n this.tableMappings = new Map();\n this.tempTables = new Set<string>();\n this.highlights = [];\n this.indexRepresentations = new Set<string>();\n this.indexesToCheck = [];\n this.highlightPositions = new Set<number>();\n this.seenReferences = new Map();\n this.shadowedAliases = [];\n this.nudges = [];\n\n Walker.traverse(root, (node, stack) => {\n const nodeNudges = parseNudges(node, stack);\n this.nudges = [...this.nudges, ...nodeNudges];\n\n // comments are not parsed here as they seem to be ignored.\n //\n // results cannot be indexed in any way because they alias a CTE\n // with alias as (select ...)\n // ^^^^^\n if (is(node, \"CommonTableExpr\")) {\n if (node.CommonTableExpr.ctename) {\n this.tempTables.add(node.CommonTableExpr.ctename);\n }\n }\n // results cannot be indexed in any way because they alias a subquery\n // select ... from (...) as alias\n // ^^^^^\n if (is(node, \"RangeSubselect\")) {\n if (node.RangeSubselect.alias?.aliasname) {\n this.tempTables.add(node.RangeSubselect.alias.aliasname);\n }\n }\n // select ... from (...) where col is null\n // ^^^^^^^\n if (is(node, \"NullTest\")) {\n if (\n node.NullTest.arg &&\n node.NullTest.nulltesttype &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n this.add(node.NullTest.arg, {\n where: { nulltest: node.NullTest.nulltesttype },\n });\n }\n }\n // The parser does not wrap the `relation` field of UpdateStmt\n // in a { RangeVar: ... } node (it's a raw RangeVar struct), so the\n // generic RangeVar handler below never sees it. Handle it explicitly.\n if (is(node, \"UpdateStmt\")) {\n if (node.UpdateStmt.relation) {\n this.registerTable(node.UpdateStmt.relation);\n }\n }\n // can be indexed as the alias refers to a regular table\n // but the alias has to be mapped to the original table name\n // select ... from table as alias\n // ^^^^^\n if (is(node, \"RangeVar\") && node.RangeVar.relname) {\n this.registerTable(node.RangeVar);\n }\n // select ... from table order by col asc\n // ^^^^^^^^^^^^^^^^\n if (is(node, \"SortBy\")) {\n // we don't care about sorting by anything that's not a column reference\n // because it couldn't be indexed anyway.\n // TODO: mark that expression as unindexable? It's just better for debugging\n if (node.SortBy.node && is(node.SortBy.node, \"ColumnRef\")) {\n this.add(node.SortBy.node, {\n sort: {\n dir: node.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\",\n nulls: node.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\",\n },\n });\n }\n }\n // select ... from table1 join table2 t2 on table1.col = t2.col\n // ^^\n if (is(node, \"JoinExpr\") && node.JoinExpr.quals) {\n if (is(node.JoinExpr.quals, \"A_Expr\")) {\n if (\n node.JoinExpr.quals.A_Expr.lexpr &&\n is(node.JoinExpr.quals.A_Expr.lexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.lexpr);\n }\n if (\n node.JoinExpr.quals.A_Expr.rexpr &&\n is(node.JoinExpr.quals.A_Expr.rexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.rexpr);\n }\n }\n }\n // select ... from table where data @> '{\"key\": \"val\"}'\n // ^^^^\n if (is(node, \"A_Expr\") && node.A_Expr.kind === \"AEXPR_OP\") {\n const opName =\n node.A_Expr.name?.[0] &&\n is(node.A_Expr.name[0], \"String\") &&\n node.A_Expr.name[0].String.sval;\n if (\n opName &&\n (opName === \"@>\" ||\n opName === \"?\" ||\n opName === \"?|\" ||\n opName === \"?&\" ||\n opName === \"@@\" ||\n opName === \"@?\")\n ) {\n const jsonbOperator = opName as JsonbOperator;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.lexpr, { jsonbOperator });\n }\n if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.rexpr, { jsonbOperator });\n }\n }\n // select ... from table where data->>'email' = 'alice@example.com'\n // ^^^^^^^^^^^^^^\n if (opName && COMPARISON_OPS.has(opName)) {\n for (const operand of [node.A_Expr.lexpr, node.A_Expr.rexpr]) {\n if (!operand) continue;\n const extraction = extractJsonbPath(operand);\n if (extraction) {\n this.add(extraction.columnRef, {\n jsonbExtraction: extraction.expression,\n });\n }\n }\n }\n }\n // any column reference anywhere\n if (is(node, \"ColumnRef\")) {\n // TODO: this approach needs refinement\n for (let i = 0; i < stack.length; i++) {\n const inReturningList =\n stack[i] === \"returningList\" &&\n stack[i + 1] === \"ResTarget\" &&\n stack[i + 2] === \"val\" &&\n stack[i + 3] === \"ColumnRef\";\n if (inReturningList) {\n this.add(node, { ignored: true });\n return;\n }\n if (\n // stack[i] === \"SelectStmt\" &&\n stack[i + 1] === \"targetList\" &&\n stack[i + 2] === \"ResTarget\" &&\n stack[i + 3] === \"val\" &&\n stack[i + 4] === \"ColumnRef\"\n ) {\n // we don't want to index the columns that are being selected\n this.add(node, { ignored: true });\n return;\n }\n\n // TODO: add functional index support here\n if (stack[i] === \"FuncCall\" && stack[i + 1] === \"args\") {\n // args of a function call can't be indexed (without functional indexes)\n this.add(node, { ignored: true });\n return;\n }\n }\n this.add(node);\n }\n });\n\n return {\n highlights: this.highlights,\n indexRepresentations: this.indexRepresentations,\n indexesToCheck: this.indexesToCheck,\n shadowedAliases: this.shadowedAliases,\n tempTables: this.tempTables,\n tableMappings: this.tableMappings,\n nudges: this.nudges,\n };\n }\n\n private add(\n node: Extract<Node, { ColumnRef: unknown }>,\n options?: {\n ignored?: boolean;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n },\n ) {\n if (!node.ColumnRef.location) {\n console.error(`Node did not have a location. Skipping`, node);\n return;\n }\n if (!node.ColumnRef.fields) {\n console.error(node);\n throw new Error(\"Column reference must have fields\");\n }\n let ignored = options?.ignored ?? false;\n let runningLength: number = node.ColumnRef.location;\n const parts: ColumnReferencePart[] = node.ColumnRef.fields.map(\n (field, i, length) => {\n if (!is(field, \"String\") || !field.String.sval) {\n const out = deparseSync(field);\n ignored = true;\n return {\n quoted: out.startsWith('\"'),\n text: out,\n start: runningLength,\n };\n }\n const start = runningLength;\n const size = field.String.sval?.length ?? 0;\n let quoted = false;\n if (node.ColumnRef.location !== undefined) {\n const boundary = this.query[runningLength];\n if (boundary === '\"') {\n quoted = true;\n }\n }\n // +1 for the dot that comes after\n const isLastIteration = i === length.length - 1;\n runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);\n return {\n text: field.String.sval,\n start,\n quoted,\n };\n },\n );\n const end = runningLength;\n if (this.highlightPositions.has(node.ColumnRef.location)) {\n return;\n }\n this.highlightPositions.add(node.ColumnRef.location);\n const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;\n const seen = this.seenReferences.get(highlighted);\n if (!ignored) {\n this.seenReferences.set(highlighted, (seen ?? 0) + 1);\n }\n const ref: DiscoveredColumnReference = {\n frequency: seen ?? 1,\n representation: highlighted,\n parts,\n ignored: ignored ?? false,\n position: {\n start: node.ColumnRef.location,\n end,\n },\n };\n if (options?.sort) {\n ref.sort = options.sort;\n }\n if (options?.where) {\n ref.where = options.where;\n }\n if (options?.jsonbOperator) {\n ref.jsonbOperator = options.jsonbOperator;\n }\n if (options?.jsonbExtraction) {\n ref.jsonbExtraction = options.jsonbExtraction;\n }\n this.highlights.push(ref);\n }\n\n private registerTable(rangeVar: RangeVar) {\n if (!rangeVar.relname) return;\n\n const columnReference: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: false,\n };\n if (rangeVar.schemaname) {\n columnReference.schema = rangeVar.schemaname;\n }\n this.tableMappings.set(rangeVar.relname, columnReference);\n\n if (rangeVar.alias?.aliasname) {\n const aliasName = rangeVar.alias.aliasname;\n const existingMapping = this.tableMappings.get(aliasName);\n const part: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: true,\n alias: aliasName,\n };\n if (rangeVar.schemaname) {\n part.schema = rangeVar.schemaname;\n }\n if (existingMapping) {\n const isSystemCatalog = rangeVar.relname?.startsWith(\"pg_\") ?? false;\n if (!isSystemCatalog) {\n console.warn(\n `Ignoring alias ${aliasName} as it shadows an existing mapping for ${existingMapping.text}. We currently do not support alias shadowing.`,\n );\n }\n this.shadowedAliases.push(part);\n return;\n }\n this.tableMappings.set(aliasName, part);\n }\n }\n\n /**\n * Descend only into shallow combinators of a node such as\n * - And\n * - Or\n * - Not\n * - ::typecast\n * without deep traversing into subqueries. Useful for checking members\n * of a `WHERE` clause\n */\n static shallowMatch<K extends KeysOfUnion<Node>>(\n expr: Node,\n kind: K,\n callback: (node: Extract<Node, Record<K, unknown>>) => void,\n ) {\n if (is(expr, kind)) {\n callback(expr as Extract<Node, Record<K, unknown>>);\n return;\n }\n if (is(expr, \"BoolExpr\") && expr.BoolExpr.args) {\n for (const arg of expr.BoolExpr.args) {\n Walker.shallowMatch(arg, kind, callback);\n }\n return;\n }\n if (is(expr, \"A_Expr\")) {\n if (expr.A_Expr.lexpr)\n Walker.shallowMatch(expr.A_Expr.lexpr, kind, callback);\n if (expr.A_Expr.rexpr)\n Walker.shallowMatch(expr.A_Expr.rexpr, kind, callback);\n return;\n }\n if (is(expr, \"NullTest\") && expr.NullTest.arg) {\n Walker.shallowMatch(expr.NullTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"BooleanTest\") && expr.BooleanTest.arg) {\n Walker.shallowMatch(expr.BooleanTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"SubLink\") && expr.SubLink.testexpr) {\n Walker.shallowMatch(expr.SubLink.testexpr, kind, callback);\n return;\n }\n if (is(expr, \"TypeCast\") && expr.TypeCast.arg) {\n Walker.shallowMatch(expr.TypeCast.arg, kind, callback);\n return;\n }\n }\n\n static traverse(\n node: Node,\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n Walker.doTraverse(node, [], callback);\n }\n\n private static doTraverse(\n node: unknown,\n stack: (KeysOfUnion<Node> | string)[],\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n if (isANode(node)) {\n callback(node, [...stack, getNodeKind(node)]);\n }\n if (typeof node !== \"object\" || node === null) {\n return;\n }\n if (Array.isArray(node)) {\n for (const item of node) {\n if (isANode(item)) {\n Walker.doTraverse(item, stack, callback);\n }\n }\n } else if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);\n } else {\n for (const [key, child] of Object.entries(node)) {\n Walker.doTraverse(\n child,\n [...stack, key as KeysOfUnion<Node>],\n callback,\n );\n }\n }\n }\n}\n\nexport type ColumnReferencePart = {\n schema?: string;\n /** the text of the column reference (excluding any potential quotes) */\n text: string;\n start?: number;\n quoted: boolean;\n alias?: string;\n};\n\ntype JsonbExtraction = {\n columnRef: Extract<Node, { ColumnRef: unknown }>;\n expression: string;\n};\n\n/**\n * Given an operand of a comparison (e.g. the left side of `=`), check whether\n * it is a JSONB path extraction expression such as `data->>'email'` or\n * `(data->>'age')::int`. If so, return the root ColumnRef (for the walker to\n * register) and the full expression string (for use in the expression index).\n *\n * Handles:\n * - `data->>'email'` — simple extraction\n * - `(data->>'age')::int` — extraction with cast\n * - `data->'addr'->>'city'` — chained extraction\n * - `t.data->>'email'` — table-qualified (strips qualifier from expression)\n */\nfunction extractJsonbPath(node: Node): JsonbExtraction | undefined {\n let exprNode = node;\n // Unwrap TypeCast (e.g. `(data->>'age')::int`)\n if (is(exprNode, \"TypeCast\") && exprNode.TypeCast.arg) {\n exprNode = exprNode.TypeCast.arg;\n }\n if (!is(exprNode, \"A_Expr\") || exprNode.A_Expr.kind !== \"AEXPR_OP\") {\n return undefined;\n }\n const innerOp =\n exprNode.A_Expr.name?.[0] &&\n is(exprNode.A_Expr.name[0], \"String\") &&\n exprNode.A_Expr.name[0].String.sval;\n if (!innerOp || !JSONB_EXTRACTION_OPS.has(innerOp)) {\n return undefined;\n }\n const rootCol = findRootColumnRef(exprNode);\n if (!rootCol) {\n return undefined;\n }\n // Deep-clone the whole outer node so we can strip qualifiers without\n // mutating the original AST.\n const cloned: Node = JSON.parse(JSON.stringify(node));\n stripTableQualifiers(cloned);\n const expression = deparseSync(cloned);\n return { columnRef: rootCol, expression };\n}\n\n/**\n * Walk the left side of a chain of `->` / `->>` operators to find the\n * root ColumnRef (the JSONB column itself).\n */\nfunction findRootColumnRef(\n node: Node,\n): Extract<Node, { ColumnRef: unknown }> | undefined {\n if (is(node, \"ColumnRef\")) {\n return node;\n }\n if (is(node, \"A_Expr\") && node.A_Expr.lexpr) {\n return findRootColumnRef(node.A_Expr.lexpr);\n }\n if (is(node, \"TypeCast\") && node.TypeCast.arg) {\n return findRootColumnRef(node.TypeCast.arg);\n }\n return undefined;\n}\n\n/**\n * Remove table/schema qualifiers from ColumnRef nodes inside the\n * expression so the deparsed expression contains only the column name.\n * e.g. `t.data->>'email'` → `data->>'email'`\n */\nfunction stripTableQualifiers(node: unknown): void {\n if (typeof node !== \"object\" || node === null) return;\n if (\n \"ColumnRef\" in (node as Record<string, unknown>) &&\n (node as any).ColumnRef?.fields\n ) {\n const fields: unknown[] = (node as any).ColumnRef.fields;\n if (fields.length > 1) {\n (node as any).ColumnRef.fields = [fields[fields.length - 1]];\n }\n return;\n }\n for (const value of Object.values(node as Record<string, unknown>)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n stripTableQualifiers(item);\n }\n } else if (typeof value === \"object\" && value !== null) {\n stripTableQualifiers(value);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"walker.mjs","names":[],"sources":["../../src/sql/walker.ts"],"sourcesContent":["import type { Node, NullTestType, RangeVar } from \"@pgsql/types\";\nimport { deparseSync } from \"pgsql-deparser\";\nimport type {\n DiscoveredColumnReference,\n JsonbOperator,\n SortContext,\n} from \"./analyzer.js\";\nimport { getNodeKind, is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { parseNudges } from \"./nudges.js\";\nimport type { Nudge } from \"./nudges.js\";\n\nconst JSONB_EXTRACTION_OPS = new Set([\"->\", \"->>\"]);\nconst COMPARISON_OPS = new Set([\n \"=\",\n \"<>\",\n \"!=\",\n \"<\",\n \"<=\",\n \">\",\n \">=\",\n \"~~\",\n \"~~*\",\n \"!~~\",\n \"!~~*\",\n]);\n\n/** Information about tables that appear in the query */\nexport type TableMappings = Map<string, ColumnReferencePart>;\ntype SeenReferences = Map<string, number>;\n\n/**\n * Walks the AST of a sql query and extracts query metadata.\n * This pattern is used to segregate the mutable state that's more common for the\n * AST walking process from the rest of the analyzer.\n */\nexport class Walker {\n private tableMappings: TableMappings = new Map();\n private tempTables = new Set<string>();\n private highlights: DiscoveredColumnReference[] = [];\n private indexRepresentations = new Set<string>();\n private indexesToCheck: DiscoveredColumnReference[] = [];\n private highlightPositions = new Set<number>();\n // used for tallying the amount of times we see stuff so\n // we have a better idea of what to start off the algorithm with\n private seenReferences: SeenReferences = new Map();\n private shadowedAliases: ColumnReferencePart[] = [];\n private nudges: Nudge[] = [];\n\n constructor(private readonly query: string) {}\n\n walk(root: Node) {\n // reset state in case the class instance is reused\n // reassigning vars here instead of using `map.clear()` to prevent\n // accidentally mutating existing references\n this.tableMappings = new Map();\n this.tempTables = new Set<string>();\n this.highlights = [];\n this.indexRepresentations = new Set<string>();\n this.indexesToCheck = [];\n this.highlightPositions = new Set<number>();\n this.seenReferences = new Map();\n this.shadowedAliases = [];\n this.nudges = [];\n\n Walker.traverse(root, (node, stack) => {\n const nodeNudges = parseNudges(node, stack);\n this.nudges = [...this.nudges, ...nodeNudges];\n\n // comments are not parsed here as they seem to be ignored.\n //\n // results cannot be indexed in any way because they alias a CTE\n // with alias as (select ...)\n // ^^^^^\n if (is(node, \"CommonTableExpr\")) {\n if (node.CommonTableExpr.ctename) {\n this.tempTables.add(node.CommonTableExpr.ctename);\n }\n }\n // results cannot be indexed in any way because they alias a subquery\n // select ... from (...) as alias\n // ^^^^^\n if (is(node, \"RangeSubselect\")) {\n if (node.RangeSubselect.alias?.aliasname) {\n this.tempTables.add(node.RangeSubselect.alias.aliasname);\n }\n }\n // select ... from (...) where col is null\n // ^^^^^^^\n if (is(node, \"NullTest\")) {\n if (\n node.NullTest.arg &&\n node.NullTest.nulltesttype &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n this.add(node.NullTest.arg, {\n where: { nulltest: node.NullTest.nulltesttype },\n });\n }\n }\n // The parser does not wrap the `relation` field of UpdateStmt\n // in a { RangeVar: ... } node (it's a raw RangeVar struct), so the\n // generic RangeVar handler below never sees it. Handle it explicitly.\n if (is(node, \"UpdateStmt\")) {\n if (node.UpdateStmt.relation) {\n this.registerTable(node.UpdateStmt.relation);\n }\n }\n // Same as UpdateStmt: DeleteStmt.relation is a raw RangeVar struct.\n if (is(node, \"DeleteStmt\")) {\n if (node.DeleteStmt.relation) {\n this.registerTable(node.DeleteStmt.relation);\n }\n }\n // can be indexed as the alias refers to a regular table\n // but the alias has to be mapped to the original table name\n // select ... from table as alias\n // ^^^^^\n if (is(node, \"RangeVar\") && node.RangeVar.relname) {\n this.registerTable(node.RangeVar);\n }\n // select ... from table order by col asc\n // ^^^^^^^^^^^^^^^^\n if (is(node, \"SortBy\")) {\n // we don't care about sorting by anything that's not a column reference\n // because it couldn't be indexed anyway.\n // TODO: mark that expression as unindexable? It's just better for debugging\n if (node.SortBy.node && is(node.SortBy.node, \"ColumnRef\")) {\n this.add(node.SortBy.node, {\n sort: {\n dir: node.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\",\n nulls: node.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\",\n },\n });\n }\n }\n // select ... from table1 join table2 t2 on table1.col = t2.col\n // ^^\n if (is(node, \"JoinExpr\") && node.JoinExpr.quals) {\n if (is(node.JoinExpr.quals, \"A_Expr\")) {\n if (\n node.JoinExpr.quals.A_Expr.lexpr &&\n is(node.JoinExpr.quals.A_Expr.lexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.lexpr);\n }\n if (\n node.JoinExpr.quals.A_Expr.rexpr &&\n is(node.JoinExpr.quals.A_Expr.rexpr, \"ColumnRef\")\n ) {\n this.add(node.JoinExpr.quals.A_Expr.rexpr);\n }\n }\n }\n // select ... from table where data @> '{\"key\": \"val\"}'\n // ^^^^\n if (is(node, \"A_Expr\") && node.A_Expr.kind === \"AEXPR_OP\") {\n const opName =\n node.A_Expr.name?.[0] &&\n is(node.A_Expr.name[0], \"String\") &&\n node.A_Expr.name[0].String.sval;\n if (\n opName &&\n (opName === \"@>\" ||\n opName === \"?\" ||\n opName === \"?|\" ||\n opName === \"?&\" ||\n opName === \"@@\" ||\n opName === \"@?\")\n ) {\n const jsonbOperator = opName as JsonbOperator;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.lexpr, { jsonbOperator });\n }\n if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"ColumnRef\")) {\n this.add(node.A_Expr.rexpr, { jsonbOperator });\n }\n }\n // select ... from table where data->>'email' = 'alice@example.com'\n // ^^^^^^^^^^^^^^\n if (opName && COMPARISON_OPS.has(opName)) {\n for (const operand of [node.A_Expr.lexpr, node.A_Expr.rexpr]) {\n if (!operand) continue;\n const extraction = extractJsonbPath(operand);\n if (extraction) {\n this.add(extraction.columnRef, {\n jsonbExtraction: extraction.expression,\n });\n }\n }\n }\n }\n // any column reference anywhere\n if (is(node, \"ColumnRef\")) {\n // TODO: this approach needs refinement\n for (let i = 0; i < stack.length; i++) {\n const inReturningList =\n stack[i] === \"returningList\" &&\n stack[i + 1] === \"ResTarget\" &&\n stack[i + 2] === \"val\" &&\n stack[i + 3] === \"ColumnRef\";\n if (inReturningList) {\n this.add(node, { ignored: true });\n return;\n }\n if (\n // stack[i] === \"SelectStmt\" &&\n stack[i + 1] === \"targetList\" &&\n stack[i + 2] === \"ResTarget\" &&\n stack[i + 3] === \"val\" &&\n stack[i + 4] === \"ColumnRef\"\n ) {\n // we don't want to index the columns that are being selected\n this.add(node, { ignored: true });\n return;\n }\n\n // TODO: add functional index support here\n if (stack[i] === \"FuncCall\" && stack[i + 1] === \"args\") {\n // args of a function call can't be indexed (without functional indexes)\n this.add(node, { ignored: true });\n return;\n }\n }\n this.add(node);\n }\n });\n\n return {\n highlights: this.highlights,\n indexRepresentations: this.indexRepresentations,\n indexesToCheck: this.indexesToCheck,\n shadowedAliases: this.shadowedAliases,\n tempTables: this.tempTables,\n tableMappings: this.tableMappings,\n nudges: this.nudges,\n };\n }\n\n private add(\n node: Extract<Node, { ColumnRef: unknown }>,\n options?: {\n ignored?: boolean;\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n },\n ) {\n if (!node.ColumnRef.location) {\n console.error(`Node did not have a location. Skipping`, node);\n return;\n }\n if (!node.ColumnRef.fields) {\n console.error(node);\n throw new Error(\"Column reference must have fields\");\n }\n let ignored = options?.ignored ?? false;\n let runningLength: number = node.ColumnRef.location;\n const parts: ColumnReferencePart[] = node.ColumnRef.fields.map(\n (field, i, length) => {\n if (!is(field, \"String\") || !field.String.sval) {\n const out = deparseSync(field);\n ignored = true;\n return {\n quoted: out.startsWith('\"'),\n text: out,\n start: runningLength,\n };\n }\n const start = runningLength;\n const size = field.String.sval?.length ?? 0;\n let quoted = false;\n if (node.ColumnRef.location !== undefined) {\n const boundary = this.query[runningLength];\n if (boundary === '\"') {\n quoted = true;\n }\n }\n // +1 for the dot that comes after\n const isLastIteration = i === length.length - 1;\n runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);\n return {\n text: field.String.sval,\n start,\n quoted,\n };\n },\n );\n const end = runningLength;\n if (this.highlightPositions.has(node.ColumnRef.location)) {\n return;\n }\n this.highlightPositions.add(node.ColumnRef.location);\n const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;\n const seen = this.seenReferences.get(highlighted);\n if (!ignored) {\n this.seenReferences.set(highlighted, (seen ?? 0) + 1);\n }\n const ref: DiscoveredColumnReference = {\n frequency: seen ?? 1,\n representation: highlighted,\n parts,\n ignored: ignored ?? false,\n position: {\n start: node.ColumnRef.location,\n end,\n },\n };\n if (options?.sort) {\n ref.sort = options.sort;\n }\n if (options?.where) {\n ref.where = options.where;\n }\n if (options?.jsonbOperator) {\n ref.jsonbOperator = options.jsonbOperator;\n }\n if (options?.jsonbExtraction) {\n ref.jsonbExtraction = options.jsonbExtraction;\n }\n this.highlights.push(ref);\n }\n\n private registerTable(rangeVar: RangeVar) {\n if (!rangeVar.relname) return;\n\n const columnReference: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: false,\n };\n if (rangeVar.schemaname) {\n columnReference.schema = rangeVar.schemaname;\n }\n this.tableMappings.set(rangeVar.relname, columnReference);\n\n if (rangeVar.alias?.aliasname) {\n const aliasName = rangeVar.alias.aliasname;\n const existingMapping = this.tableMappings.get(aliasName);\n const part: ColumnReferencePart = {\n text: rangeVar.relname,\n start: rangeVar.location,\n quoted: true,\n alias: aliasName,\n };\n if (rangeVar.schemaname) {\n part.schema = rangeVar.schemaname;\n }\n if (existingMapping) {\n const isSystemCatalog = rangeVar.relname?.startsWith(\"pg_\") ?? false;\n if (!isSystemCatalog) {\n console.warn(\n `Ignoring alias ${aliasName} as it shadows an existing mapping for ${existingMapping.text}. We currently do not support alias shadowing.`,\n );\n }\n this.shadowedAliases.push(part);\n return;\n }\n this.tableMappings.set(aliasName, part);\n }\n }\n\n /**\n * Descend only into shallow combinators of a node such as\n * - And\n * - Or\n * - Not\n * - ::typecast\n * without deep traversing into subqueries. Useful for checking members\n * of a `WHERE` clause\n */\n static shallowMatch<K extends KeysOfUnion<Node>>(\n expr: Node,\n kind: K,\n callback: (node: Extract<Node, Record<K, unknown>>) => void,\n ) {\n if (is(expr, kind)) {\n callback(expr as Extract<Node, Record<K, unknown>>);\n return;\n }\n if (is(expr, \"BoolExpr\") && expr.BoolExpr.args) {\n for (const arg of expr.BoolExpr.args) {\n Walker.shallowMatch(arg, kind, callback);\n }\n return;\n }\n if (is(expr, \"A_Expr\")) {\n if (expr.A_Expr.lexpr)\n Walker.shallowMatch(expr.A_Expr.lexpr, kind, callback);\n if (expr.A_Expr.rexpr)\n Walker.shallowMatch(expr.A_Expr.rexpr, kind, callback);\n return;\n }\n if (is(expr, \"NullTest\") && expr.NullTest.arg) {\n Walker.shallowMatch(expr.NullTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"BooleanTest\") && expr.BooleanTest.arg) {\n Walker.shallowMatch(expr.BooleanTest.arg, kind, callback);\n return;\n }\n if (is(expr, \"SubLink\") && expr.SubLink.testexpr) {\n Walker.shallowMatch(expr.SubLink.testexpr, kind, callback);\n return;\n }\n if (is(expr, \"TypeCast\") && expr.TypeCast.arg) {\n Walker.shallowMatch(expr.TypeCast.arg, kind, callback);\n return;\n }\n }\n\n static traverse(\n node: Node,\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n Walker.doTraverse(node, [], callback);\n }\n\n private static doTraverse(\n node: unknown,\n stack: (KeysOfUnion<Node> | string)[],\n callback: (node: Node, stack: (KeysOfUnion<Node> | string)[]) => void,\n ) {\n if (isANode(node)) {\n callback(node, [...stack, getNodeKind(node)]);\n }\n if (typeof node !== \"object\" || node === null) {\n return;\n }\n if (Array.isArray(node)) {\n for (const item of node) {\n if (isANode(item)) {\n Walker.doTraverse(item, stack, callback);\n }\n }\n } else if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);\n } else {\n for (const [key, child] of Object.entries(node)) {\n Walker.doTraverse(\n child,\n [...stack, key as KeysOfUnion<Node>],\n callback,\n );\n }\n }\n }\n}\n\nexport type ColumnReferencePart = {\n schema?: string;\n /** the text of the column reference (excluding any potential quotes) */\n text: string;\n start?: number;\n quoted: boolean;\n alias?: string;\n};\n\ntype JsonbExtraction = {\n columnRef: Extract<Node, { ColumnRef: unknown }>;\n expression: string;\n};\n\n/**\n * Given an operand of a comparison (e.g. the left side of `=`), check whether\n * it is a JSONB path extraction expression such as `data->>'email'` or\n * `(data->>'age')::int`. If so, return the root ColumnRef (for the walker to\n * register) and the full expression string (for use in the expression index).\n *\n * Handles:\n * - `data->>'email'` — simple extraction\n * - `(data->>'age')::int` — extraction with cast\n * - `data->'addr'->>'city'` — chained extraction\n * - `t.data->>'email'` — table-qualified (strips qualifier from expression)\n */\nfunction extractJsonbPath(node: Node): JsonbExtraction | undefined {\n let exprNode = node;\n // Unwrap TypeCast (e.g. `(data->>'age')::int`)\n if (is(exprNode, \"TypeCast\") && exprNode.TypeCast.arg) {\n exprNode = exprNode.TypeCast.arg;\n }\n if (!is(exprNode, \"A_Expr\") || exprNode.A_Expr.kind !== \"AEXPR_OP\") {\n return undefined;\n }\n const innerOp =\n exprNode.A_Expr.name?.[0] &&\n is(exprNode.A_Expr.name[0], \"String\") &&\n exprNode.A_Expr.name[0].String.sval;\n if (!innerOp || !JSONB_EXTRACTION_OPS.has(innerOp)) {\n return undefined;\n }\n const rootCol = findRootColumnRef(exprNode);\n if (!rootCol) {\n return undefined;\n }\n // Deep-clone the whole outer node so we can strip qualifiers without\n // mutating the original AST.\n const cloned: Node = JSON.parse(JSON.stringify(node));\n stripTableQualifiers(cloned);\n const expression = deparseSync(cloned);\n return { columnRef: rootCol, expression };\n}\n\n/**\n * Walk the left side of a chain of `->` / `->>` operators to find the\n * root ColumnRef (the JSONB column itself).\n */\nfunction findRootColumnRef(\n node: Node,\n): Extract<Node, { ColumnRef: unknown }> | undefined {\n if (is(node, \"ColumnRef\")) {\n return node;\n }\n if (is(node, \"A_Expr\") && node.A_Expr.lexpr) {\n return findRootColumnRef(node.A_Expr.lexpr);\n }\n if (is(node, \"TypeCast\") && node.TypeCast.arg) {\n return findRootColumnRef(node.TypeCast.arg);\n }\n return undefined;\n}\n\n/**\n * Remove table/schema qualifiers from ColumnRef nodes inside the\n * expression so the deparsed expression contains only the column name.\n * e.g. `t.data->>'email'` → `data->>'email'`\n */\nfunction stripTableQualifiers(node: unknown): void {\n if (typeof node !== \"object\" || node === null) return;\n if (\n \"ColumnRef\" in (node as Record<string, unknown>) &&\n (node as any).ColumnRef?.fields\n ) {\n const fields: unknown[] = (node as any).ColumnRef.fields;\n if (fields.length > 1) {\n (node as any).ColumnRef.fields = [fields[fields.length - 1]];\n }\n return;\n }\n for (const value of Object.values(node as Record<string, unknown>)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n stripTableQualifiers(item);\n }\n } else if (typeof value === \"object\" && value !== null) {\n stripTableQualifiers(value);\n }\n }\n}\n"],"mappings":"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"}
|