@query-doctor/core 0.8.8 → 0.8.9-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/optimizer/statistics.cjs +1 -1
- package/dist/optimizer/statistics.cjs.map +1 -1
- package/dist/optimizer/statistics.mjs +1 -1
- package/dist/optimizer/statistics.mjs.map +1 -1
- package/dist/sql/nudges.cjs +5 -0
- package/dist/sql/nudges.cjs.map +1 -1
- package/dist/sql/nudges.d.cts +1 -1
- package/dist/sql/nudges.d.cts.map +1 -1
- package/dist/sql/nudges.d.mts +1 -1
- package/dist/sql/nudges.d.mts.map +1 -1
- package/dist/sql/nudges.mjs +5 -0
- package/dist/sql/nudges.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -606,7 +606,7 @@ var Statistics = class Statistics {
|
|
|
606
606
|
static async dumpStats(db, postgresVersion) {
|
|
607
607
|
console.log(`dumping stats for postgres ${(0, colorette.gray)(postgresVersion)}`);
|
|
608
608
|
const stats = await db.exec(DUMP_STATS_SQL);
|
|
609
|
-
return zod.z.array(ExportedStats).parse(stats[0].json_agg);
|
|
609
|
+
return zod.z.array(ExportedStats).parse(stats[0].json_agg ?? []);
|
|
610
610
|
}
|
|
611
611
|
/**
|
|
612
612
|
* Returns all indexes in the database.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statistics.cjs","names":["z","PgIdentifier"],"sources":["../../src/optimizer/statistics.ts"],"sourcesContent":["import { gray } from \"colorette\";\nimport dedent from \"dedent\";\nimport { z } from \"zod\";\nimport type {\n Postgres,\n PostgresTransaction,\n PostgresVersion,\n} from \"../sql/database.ts\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexToCreate } from \"./genalgo.js\";\n\ntype StaValueKind = \"real\" | \"text\" | \"boolean\" | null;\n\nexport type Path = string;\n\nexport const StatisticsSource = z.union([\n z.object({\n kind: z.literal(\"path\"),\n path: z.string().min(1),\n }),\n z.object({\n kind: z.literal(\"inline\"),\n }),\n]);\n\nexport const ExportedStatsStatistics = z.object({\n stawidth: z.number(),\n stainherit: z.boolean().default(false),\n // 0 representing unknown\n stadistinct: z.number(),\n // this has no \"nullable\" state\n stanullfrac: z.number(),\n stakind1: z.number().min(0),\n stakind2: z.number().min(0),\n stakind3: z.number().min(0),\n stakind4: z.number().min(0),\n stakind5: z.number().min(0),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n // theoretically... this could only be strings and numbers\n // but we don't have a crystal ball\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n});\n\nexport const ExportedStatsColumns = z.object({\n columnName: z.string(),\n attlen: z.number().nullable(),\n dataType: z.string().optional(),\n stats: ExportedStatsStatistics.nullable(),\n});\n\nexport const ExportedStatsIndex = z.object({\n indexName: z.string(),\n amname: z.string().default(\"btree\"),\n relpages: z.number(),\n reltuples: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n fillfactor: z.number().default(90),\n columns: z.array(z.object({ attlen: z.number().nullable() })).default([]),\n});\n\n// This should match the output of the `_qd_dump_stats` function in the analyzer README.md\n// Need to make sure this is versioned to accept ALL potential outputs from every version of\n// dump functions we make public\nexport const ExportedStatsV1 = z.object({\n tableName: z.string(),\n schemaName: z.string(),\n // can be negative\n relpages: z.number(),\n // can be negative\n reltuples: z.number(),\n relallvisible: z.number(),\n // only postgres 18+\n relallfrozen: z.number().optional(),\n columns: z.array(ExportedStatsColumns).default([]),\n indexes: z.array(ExportedStatsIndex),\n});\n\nexport const ExportedStats = z.union([ExportedStatsV1]);\n\nexport type ExportedStats = z.infer<typeof ExportedStats>;\n\nexport const StatisticsMode = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"fromAssumption\"),\n reltuples: z.number().min(0),\n }),\n z.object({\n kind: z.literal(\"fromStatisticsExport\"),\n stats: z.array(ExportedStats),\n source: StatisticsSource,\n }),\n]);\n\nexport type StatisticsMode = z.infer<typeof StatisticsMode>;\n\nexport const ComputedColumnStats = z.object({\n schema_name: z.string(),\n table_name: z.string(),\n column_name: z.string(),\n data_type: z.string().optional(),\n stainherit: z.boolean(),\n stanullfrac: z.number(),\n stawidth: z.number(),\n stadistinct: z.number(),\n stakind1: z.number(),\n stakind2: z.number(),\n stakind3: z.number(),\n stakind4: z.number(),\n stakind5: z.number(),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n _value_type1: z.string().nullable(),\n _value_type2: z.string().nullable(),\n _value_type3: z.string().nullable(),\n _value_type4: z.string().nullable(),\n _value_type5: z.string().nullable(),\n});\n\nexport type ComputedColumnStats = z.infer<typeof ComputedColumnStats>;\n\nexport const ComputedReltuples = z.object({\n relname: z.string(),\n schema_name: z.string(),\n reltuples: z.number(),\n relpages: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n});\n\nexport type ComputedReltuples = z.infer<typeof ComputedReltuples>;\n\nexport const ComputedStats = z.object({\n columnStats: z.array(ComputedColumnStats),\n reltuples: z.array(ComputedReltuples),\n});\n\nexport type ComputedStats = z.infer<typeof ComputedStats>;\n\nconst DEFAULT_RELTUPLES = 10_000_000;\nconst DEFAULT_RELPAGES = 1;\n// it's _very_ rare that the default page size is ever changed\nconst DEFAULT_PAGE_SIZE = 2 ** 13;\n\nfunction estimateStawidth(col: { attlen?: number | null }): number {\n return col.attlen ?? 32;\n}\n\nfunction estimateRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n): number {\n const rowWidth =\n // 23 byte tuple header + 4 bytes alignment/null bitmap\n columns.reduce((sum, col) => sum + estimateStawidth(col), 0) + 27;\n return Math.ceil((reltuples * rowWidth) / DEFAULT_PAGE_SIZE);\n}\n\nfunction estimateIndexRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n fillfactor: number,\n amname: string,\n tableRelpages: number,\n): number {\n if (amname === \"gin\") {\n // GIN has an inverted structure; distinct element counts per row are unknown\n // without real data, so fall back to a ratio of the table page count\n return Math.ceil(tableRelpages * 0.3);\n }\n // 16 bytes btree entry overhead per key in addition to the key width\n const keyWidth = columns.reduce(\n (sum, col) => sum + estimateStawidth(col) + 16,\n 0,\n );\n return Math.ceil((reltuples * keyWidth) / DEFAULT_PAGE_SIZE / fillfactor);\n}\n\nexport const DUMP_STATS_SQL = dedent`\n WITH table_columns AS (\n SELECT\n cl.relname,\n n.nspname,\n cl.reltuples,\n cl.relpages,\n cl.relallvisible,\n -- cl.relallfrozen,\n json_agg(\n json_build_object(\n 'columnName', a.attname,\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END,\n 'dataType', t.typname,\n 'stats', (\n SELECT json_build_object(\n 'starelid', s.starelid,\n 'staattnum', s.staattnum,\n 'stanullfrac', s.stanullfrac,\n 'stawidth', s.stawidth,\n 'stadistinct', s.stadistinct,\n 'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,\n 'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,\n 'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,\n 'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,\n 'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,\n 'stavalues1', s.stavalues1,\n 'stavalues2', s.stavalues2,\n 'stavalues3', s.stavalues3,\n 'stavalues4', s.stavalues4,\n 'stavalues5', s.stavalues5\n )\n FROM pg_statistic s\n WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum\n )\n )\n ORDER BY a.attnum\n ) AS columns\n FROM pg_class cl\n JOIN pg_namespace n ON n.oid = cl.relnamespace\n JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attnum > 0 AND NOT a.attisdropped\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE cl.relkind = 'r'\n AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'tiger', 'tiger_data', 'topology')\n AND cl.relname NOT IN ('pg_stat_statements', 'pg_stat_statements_info')\n GROUP BY cl.relname, n.nspname, cl.reltuples, cl.relpages, cl.relallvisible\n ),\n table_indexes AS (\n SELECT\n t.relname AS table_name,\n json_agg(\n json_build_object(\n 'indexName', i.relname,\n 'amname', am.amname,\n 'reltuples', i.reltuples,\n 'relpages', i.relpages,\n 'relallvisible', i.relallvisible,\n -- 'relallfrozen', i.relallfrozen,\n 'fillfactor', COALESCE(\n (\n SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1\n ),\n 90\n ),\n 'columns', COALESCE(\n (\n SELECT json_agg(json_build_object(\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END\n ) ORDER BY col_pos.ord)\n FROM unnest(ix.indkey) WITH ORDINALITY AS col_pos(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = col_pos.attnum\n WHERE col_pos.attnum > 0\n ),\n '[]'::json\n )\n )\n ) AS indexes\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON am.oid = i.relam\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE t.relname NOT LIKE 'pg_%'\n AND n.nspname <> 'information_schema'\n AND n.nspname NOT IN ('tiger', 'tiger_data', 'topology')\n GROUP BY t.relname\n )\n SELECT json_agg(\n json_build_object(\n 'tableName', tc.relname,\n 'schemaName', tc.nspname,\n 'reltuples', tc.reltuples,\n 'relpages', tc.relpages,\n 'relallvisible', tc.relallvisible,\n -- 'relallfrozen', tc.relallfrozen,\n 'columns', COALESCE(tc.columns, '[]'::json),\n 'indexes', COALESCE(ti.indexes, '[]'::json)\n )\n )\n FROM table_columns tc\n LEFT JOIN table_indexes ti\n ON ti.table_name = tc.relname\n`;\n\nexport class Statistics {\n readonly mode: StatisticsMode;\n readonly computedStats: ComputedStats;\n private readonly exportedMetadata: ExportedStats[] | undefined;\n private additionalIndexes: IndexToCreate[] = [];\n // preventing accidental internal mutations\n static readonly defaultStatsMode: StatisticsMode = Object.freeze({\n kind: \"fromAssumption\",\n reltuples: DEFAULT_RELTUPLES,\n });\n constructor(\n private readonly db: Postgres,\n public readonly postgresVersion: PostgresVersion,\n public readonly ownMetadata: ExportedStats[],\n statsMode: StatisticsMode,\n ) {\n if (statsMode) {\n this.mode = statsMode;\n if (statsMode.kind === \"fromStatisticsExport\") {\n this.exportedMetadata = statsMode.stats;\n }\n } else {\n this.mode = Statistics.defaultStatsMode;\n }\n this.computedStats = this.buildComputedStats();\n }\n\n setAdditionalIndexes(additionalIndexes: IndexToCreate[]): void {\n this.additionalIndexes = additionalIndexes;\n }\n\n private buildComputedStats(): ComputedStats {\n const columnStats: ComputedColumnStats[] = [];\n const reltuples: ComputedReltuples[] = [];\n\n for (const table of this.ownMetadata) {\n const targetTable = this.exportedMetadata?.find(\n (m) =>\n m.tableName === table.tableName && m.schemaName === table.schemaName,\n );\n const target = targetTable?.columns ?? table.columns;\n\n for (const column of target) {\n const { stats } = column;\n if (!stats || this.mode.kind === \"fromAssumption\") {\n const stawidth = stats?.stawidth || estimateStawidth(column);\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: false,\n stanullfrac: 0.04,\n stawidth,\n stadistinct: -0.9,\n stakind1: 0,\n stakind2: 0,\n stakind3: 3,\n stakind4: 0,\n stakind5: 0,\n stacoll1: \"0\",\n stacoll2: \"0\",\n stacoll3: \"0\",\n stacoll4: \"0\",\n stacoll5: \"0\",\n staop1: \"0\",\n staop2: \"0\",\n staop3: \"0\",\n staop4: \"0\",\n staop5: \"0\",\n stanumbers1: null,\n stanumbers2: null,\n stanumbers3: [0.9],\n stanumbers4: null,\n stanumbers5: null,\n stavalues1: null,\n stavalues2: null,\n stavalues3: null,\n stavalues4: null,\n stavalues5: null,\n _value_type1: \"real\",\n _value_type2: \"real\",\n _value_type3: \"real\",\n _value_type4: \"real\",\n _value_type5: \"real\",\n });\n } else {\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: stats.stainherit ?? false,\n stanullfrac: stats.stanullfrac,\n stawidth: stats.stawidth,\n stadistinct: stats.stadistinct,\n stakind1: stats.stakind1,\n stakind2: stats.stakind2,\n stakind3: stats.stakind3,\n stakind4: stats.stakind4,\n stakind5: stats.stakind5,\n staop1: stats.staop1,\n staop2: stats.staop2,\n staop3: stats.staop3,\n staop4: stats.staop4,\n staop5: stats.staop5,\n stacoll1: stats.stacoll1,\n stacoll2: stats.stacoll2,\n stacoll3: stats.stacoll3,\n stacoll4: stats.stacoll4,\n stacoll5: stats.stacoll5,\n stanumbers1: stats.stanumbers1,\n stanumbers2: stats.stanumbers2,\n stanumbers3: stats.stanumbers3,\n stanumbers4: stats.stanumbers4,\n stanumbers5: stats.stanumbers5,\n stavalues1: Statistics.safeStavalues(stats.stavalues1),\n stavalues2: Statistics.safeStavalues(stats.stavalues2),\n stavalues3: Statistics.safeStavalues(stats.stavalues3),\n stavalues4: Statistics.safeStavalues(stats.stavalues4),\n stavalues5: Statistics.safeStavalues(stats.stavalues5),\n _value_type1: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues1),\n ),\n _value_type2: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues2),\n ),\n _value_type3: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues3),\n ),\n _value_type4: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues4),\n ),\n _value_type5: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues5),\n ),\n });\n }\n }\n\n let tableReltuples: number;\n let tableRelpages: number;\n let relallvisible = 0;\n let relallfrozen: number | undefined;\n\n if (this.mode.kind === \"fromAssumption\") {\n tableReltuples = this.mode.reltuples;\n tableRelpages = estimateRelpages(tableReltuples, table.columns);\n } else if (targetTable) {\n tableReltuples = targetTable.reltuples;\n tableRelpages = targetTable.relpages;\n relallvisible = targetTable.relallvisible;\n relallfrozen = targetTable.relallfrozen;\n } else {\n tableReltuples = DEFAULT_RELTUPLES;\n tableRelpages = DEFAULT_RELPAGES;\n }\n\n reltuples.push({\n relname: table.tableName,\n schema_name: table.schemaName,\n reltuples: tableReltuples,\n relpages: tableRelpages,\n relallfrozen,\n relallvisible,\n });\n\n if (this.mode.kind === \"fromAssumption\") {\n for (const index of table.indexes) {\n const indexRelpages = estimateIndexRelpages(\n this.mode.reltuples,\n index.columns,\n index.fillfactor / 100,\n index.amname,\n tableRelpages,\n );\n reltuples.push({\n relname: index.indexName,\n schema_name: table.schemaName,\n reltuples: this.mode.reltuples,\n relpages: indexRelpages,\n relallfrozen: 0,\n relallvisible: indexRelpages,\n });\n }\n } else if (targetTable) {\n for (const index of targetTable.indexes) {\n reltuples.push({\n relname: index.indexName,\n schema_name: targetTable.schemaName,\n reltuples: index.reltuples,\n relpages: index.relpages,\n relallfrozen: index.relallfrozen,\n relallvisible: index.relallvisible,\n });\n }\n }\n }\n\n return { columnStats, reltuples };\n }\n\n static statsModeFromAssumption({\n reltuples,\n }: {\n reltuples: number;\n }): StatisticsMode {\n return {\n kind: \"fromAssumption\",\n reltuples,\n };\n }\n\n /**\n * Create a statistic mode from stats exported from another database\n **/\n static statsModeFromExport(stats: ExportedStats[]): StatisticsMode {\n return {\n kind: \"fromStatisticsExport\",\n source: { kind: \"inline\" },\n stats,\n };\n }\n\n static async fromPostgres(\n db: Postgres,\n statsMode: StatisticsMode,\n ): Promise<Statistics> {\n const version = await db.serverNum();\n const ownStats = await Statistics.dumpStats(db, version);\n return new Statistics(db, version, ownStats, statsMode);\n }\n\n restoreStats(tx: PostgresTransaction) {\n // if (this.postgresVersion < \"180000\") {\n return this.restoreStats17(tx);\n // }\n // return this.restoreStats18(tx);\n }\n\n approximateTotalRows() {\n if (!this.exportedMetadata) {\n return 0;\n }\n let totalRows = 0;\n for (const table of this.exportedMetadata) {\n totalRows += table.reltuples;\n }\n return totalRows;\n }\n\n /**\n * We have to cast stavaluesN to the correct type\n * This derives that type for us so it can be used in `array_in`\n */\n private stavalueKind(values: unknown[] | null): StaValueKind {\n if (!values || values.length === 0) {\n return null;\n }\n const [elem] = values;\n if (typeof elem === \"number\") {\n return \"real\";\n } else if (typeof elem === \"boolean\") {\n return \"boolean\";\n }\n // is everything else a text? What about strinfied dates?\n // we might need column metadata access here if we do\n return \"text\";\n }\n\n /**\n * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays\n * for columns with array types (e.g. text[], int4[]). These create\n * multidimensional arrays that can be \"ragged\" (sub-arrays with different\n * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional\n * arrays from JSON, so we need to drop these values.\n */\n private static safeStavalues(values: unknown[] | null): unknown[] | null {\n if (!values || values.length === 0) return values;\n if (values.some((v) => Array.isArray(v))) {\n console.warn(\"Discarding ragged multidimensional stavalues array\");\n return null;\n }\n return values;\n }\n\n /**\n * When inserting fake stats for existing tables and indexes, we also need to\n * account for data on newly created indexes by the optimizer.\n *\n * However we assume that index reltuples = table reltuples.\n * Meaning this logic is going to be a little off for partial indexes\n * or posting lists for duplicate values in btrees.\n * Meaning the deduplication that happens in pg side\n * 4 reltuples [(2, 1),(2, 9),(2, 4),(2, 8)] -> 1 reltuple [2, (1, 9, 4, 8)]\n * does not get accounted for here.\n */\n private async getAdditionalIndexReltuples(\n tx: PostgresTransaction,\n ): Promise<ComputedReltuples[]> {\n if (this.additionalIndexes.length === 0) {\n return [];\n }\n\n type ColumnAttlenRow = {\n index_name: string;\n schema_name: string;\n table_name: string;\n column_name: string;\n attlen: number | null;\n fillfactor: number;\n };\n\n const indexNames = this.additionalIndexes.map((idx) => idx.name.toString());\n const rows = await tx\n .exec<ColumnAttlenRow>(\n dedent`\n SELECT\n i.relname AS index_name,\n n.nspname AS schema_name,\n t.relname AS table_name,\n a.attname AS column_name,\n CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END AS attlen,\n COALESCE(\n (SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1),\n 90\n ) AS fillfactor\n FROM pg_class i\n JOIN pg_index ix ON ix.indexrelid = i.oid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN unnest(ix.indkey) AS k(attnum) ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE i.relname IN (SELECT jsonb_array_elements_text($1::jsonb))\n AND k.attnum > 0\n `,\n [indexNames],\n )\n .catch((err) => {\n console.error(\n \"Something went wrong querying additional index column metadata\",\n );\n console.error(err);\n return [];\n });\n\n const attlenByKey = new Map<string, number | null>();\n const fillfactorByIndex = new Map<string, number>();\n for (const row of rows) {\n attlenByKey.set(\n `${row.schema_name}.${row.table_name}.${row.column_name}`,\n row.attlen,\n );\n fillfactorByIndex.set(row.index_name, row.fillfactor);\n }\n\n return this.additionalIndexes.flatMap((idx) => {\n const tableStats = this.computedStats.reltuples.find((r) => {\n // making sure to follow normalization rules\n const sourceSchema = PgIdentifier.fromString(r.schema_name).toString();\n const sourceTable = PgIdentifier.fromString(r.relname).toString();\n\n const targetSchema = PgIdentifier.fromString(idx.schema).toString();\n const targetTable = PgIdentifier.fromString(idx.table).toString();\n return sourceSchema === targetSchema && sourceTable === targetTable;\n });\n\n if (!tableStats) {\n return [];\n }\n const columns = idx.columns.map((col) => ({\n attlen:\n attlenByKey.get(`${idx.schema}.${idx.table}.${col.column}`) ?? null,\n }));\n const fillfactor = fillfactorByIndex.get(idx.name.toString()) ?? 90;\n const amname = idx.indexMethod ?? \"btree\";\n const relpages = estimateIndexRelpages(\n tableStats.reltuples,\n columns,\n fillfactor / 100,\n amname,\n tableStats.relpages,\n );\n\n return [\n {\n relname: idx.name.toString(),\n schema_name: idx.schema,\n reltuples: tableStats.reltuples,\n relpages,\n relallvisible: relpages,\n relallfrozen: 0,\n },\n ];\n });\n }\n\n private async restoreStats17(tx: PostgresTransaction) {\n const warnings = {\n tablesNotInExports: [] as string[],\n tablesNotInTest: [] as string[],\n tableNotAnalyzed: [] as string[],\n statsMissing: [] as {\n statistic: string;\n table: string;\n schema: string;\n column: string;\n }[],\n };\n\n const processedTables = new Set<string>(\n this.ownMetadata.map((t) => `${t.schemaName}.${t.tableName}`),\n );\n\n const columnStatsUpdatePromise = tx\n .exec(Statistics.columnStatsSQL, [this.computedStats.columnStats])\n .catch((err) => {\n console.error(\"Something wrong wrong updating column stats\");\n console.error(err);\n throw err;\n });\n\n /**\n * Postgres has 5 different slots for storing statistics per column and a potentially unlimited\n * number of statistic types to choose from. Each code in `stakindN` can mean different things.\n * Some statistics are just numerical values such as `n_distinct` and `correlation`, meaning\n * they're only derived from `stanumbersN` and the value of `stanumbersN` is never read.\n * Others take advantage of the `stavaluesN` columns which use `anyarray` type to store\n * concrete values internally for things like histogram bounds.\n * Unfortunately we cannot change anyarrays without a C extension.\n *\n * (1) = most common values\n * (2) = scalar histogram\n * (3) = correlation <- can change\n * (4) = most common elements\n * (5) = distinct elem count histogram <- can change\n * (6) = length histogram (?) These don't appear in pg_stats\n * (7) = bounds histogram (?) These don't appear in pg_stats\n * (N) = potentially many more kinds of statistics. But postgres <=18 only uses these 7.\n *\n * What we're doing here is setting ANY statistic we cannot directly control\n * (anything that relies on stavaluesN) to 0 to make sure the planner isn't influenced by what\n * what the db collected from the test data.\n * Because we do our tests with `generic_plan` it seems it's already unlikely that the planner will be\n * using things like common values or histogram bounds to make the planning decisions we care about.\n * This is a just in case.\n */\n const reltuplesQuery = dedent`\n update pg_class p\n set reltuples = v.reltuples,\n relpages = v.relpages,\n -- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,\n relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end\n from jsonb_to_recordset($1::jsonb)\n as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)\n where p.relname = v.relname\n and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)\n returning p.relname, p.relnamespace, p.reltuples, p.relpages;\n `;\n\n const additionalIndexReltuples = await this.getAdditionalIndexReltuples(tx);\n\n const reltuplesPromise = tx\n .exec(reltuplesQuery, [\n [...this.computedStats.reltuples, ...additionalIndexReltuples],\n ])\n .catch((err) => {\n console.error(\"Something went wrong updating reltuples/relpages\");\n console.error(err);\n return err;\n });\n\n if (this.exportedMetadata) {\n for (const table of this.exportedMetadata) {\n const tableExists = processedTables.has(\n `${table.schemaName}.${table.tableName}`,\n );\n if (tableExists && table.reltuples === -1) {\n console.warn(\n `Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`,\n );\n warnings.tableNotAnalyzed.push(\n `${table.schemaName}.${table.tableName}`,\n );\n }\n if (tableExists) {\n continue;\n }\n warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);\n }\n }\n const [statsUpdates, reltuplesUpdates] = await Promise.all([\n columnStatsUpdatePromise,\n reltuplesPromise,\n ]);\n const updatedColumnsProperly = statsUpdates\n ? statsUpdates.length === this.computedStats.columnStats.length\n : true;\n if (!updatedColumnsProperly) {\n console.error(`Did not update expected column stats`);\n }\n if (reltuplesUpdates.length !== this.computedStats.reltuples.length) {\n console.error(`Did not update expected reltuples/relpages`);\n }\n return warnings;\n }\n\n private static readonly columnStatsSQL = dedent`\n WITH input AS (\n SELECT\n c.oid AS starelid,\n a.attnum AS staattnum,\n v.stainherit,\n v.stanullfrac,\n v.stawidth,\n v.stadistinct,\n v.stakind1,\n v.stakind2,\n v.stakind3,\n v.stakind4,\n v.stakind5,\n v.staop1,\n v.staop2,\n v.staop3,\n v.staop4,\n v.staop5,\n v.stacoll1,\n v.stacoll2,\n v.stacoll3,\n v.stacoll4,\n v.stacoll5,\n v.stanumbers1,\n v.stanumbers2,\n v.stanumbers3,\n v.stanumbers4,\n v.stanumbers5,\n v.stavalues1,\n v.stavalues2,\n v.stavalues3,\n v.stavalues4,\n v.stavalues5,\n _value_type1,\n _value_type2,\n _value_type3,\n _value_type4,\n _value_type5\n FROM jsonb_to_recordset($1::jsonb) AS v(\n schema_name text,\n table_name text,\n column_name text,\n stainherit boolean,\n stanullfrac real,\n stawidth integer,\n stadistinct real,\n stakind1 real,\n stakind2 real,\n stakind3 real,\n stakind4 real,\n stakind5 real,\n staop1 oid,\n staop2 oid,\n staop3 oid,\n staop4 oid,\n staop5 oid,\n stacoll1 oid,\n stacoll2 oid,\n stacoll3 oid,\n stacoll4 oid,\n stacoll5 oid,\n stanumbers1 real[],\n stanumbers2 real[],\n stanumbers3 real[],\n stanumbers4 real[],\n stanumbers5 real[],\n stavalues1 text[],\n stavalues2 text[],\n stavalues3 text[],\n stavalues4 text[],\n stavalues5 text[],\n _value_type1 text,\n _value_type2 text,\n _value_type3 text,\n _value_type4 text,\n _value_type5 text\n )\n JOIN pg_class c ON c.relname = v.table_name\n JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name\n ),\n updated AS (\n UPDATE pg_statistic s\n SET\n stanullfrac = i.stanullfrac,\n stawidth = i.stawidth,\n stadistinct = i.stadistinct,\n stakind1 = i.stakind1,\n stakind2 = i.stakind2,\n stakind3 = i.stakind3,\n stakind4 = i.stakind4,\n stakind5 = i.stakind5,\n staop1 = i.staop1,\n staop2 = i.staop2,\n staop3 = i.staop3,\n staop4 = i.staop4,\n staop5 = i.staop5,\n stacoll1 = i.stacoll1,\n stacoll2 = i.stacoll2,\n stacoll3 = i.stacoll3,\n stacoll4 = i.stacoll4,\n stacoll5 = i.stacoll5,\n stanumbers1 = i.stanumbers1,\n stanumbers2 = i.stanumbers2,\n stanumbers3 = i.stanumbers3,\n stanumbers4 = i.stanumbers4,\n stanumbers5 = i.stanumbers5,\n stavalues1 = case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n stavalues2 = case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n stavalues3 = case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n stavalues4 = case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n stavalues5 = case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- stavalues1 = i.stavalues1,\n -- stavalues2 = i.stavalues2,\n -- stavalues3 = i.stavalues3,\n -- stavalues4 = i.stavalues4,\n -- stavalues5 = i.stavalues5\n FROM input i\n WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit\n RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5\n ),\n inserted as (\n INSERT INTO pg_statistic (\n starelid, staattnum, stainherit,\n stanullfrac, stawidth, stadistinct,\n stakind1, stakind2, stakind3, stakind4, stakind5,\n staop1, staop2, staop3, staop4, staop5,\n stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,\n stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,\n stavalues1, stavalues2, stavalues3, stavalues4, stavalues5\n )\n SELECT\n i.starelid, i.staattnum, i.stainherit,\n i.stanullfrac, i.stawidth, i.stadistinct,\n i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,\n i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,\n i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,\n i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,\n -- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,\n case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5\n FROM input i\n LEFT JOIN updated u\n ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit\n WHERE u.starelid IS NULL\n returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5\n )\n select * from updated union all (select * from inserted); -- @qd_introspection`;\n\n static async dumpStats(\n db: PostgresTransaction,\n postgresVersion: PostgresVersion,\n ): Promise<ExportedStats[]> {\n console.log(`dumping stats for postgres ${gray(postgresVersion)}`);\n const stats = await db.exec<{ json_agg: ExportedStats[] }>(DUMP_STATS_SQL);\n const out = z.array(ExportedStats).parse(stats[0].json_agg);\n return out;\n }\n\n /**\n * Returns all indexes in the database.\n * ONLY handles regular btree indexes\n */\n async getExistingIndexes(): Promise<IndexedTable[]> {\n const indexes = await this.db.exec<IndexedTable>(`\n WITH partitioned_tables AS (\n SELECT\n inhparent::regclass AS parent_table,\n inhrelid::regclass AS partition_table\n FROM\n pg_inherits\n )\n SELECT\n n.nspname AS schema_name,\n COALESCE(pt.parent_table::text, t.relname) AS table_name,\n i.relname AS index_name,\n ix.indisprimary as is_primary,\n ix.indisunique as is_unique,\n am.amname AS index_type,\n array_agg(\n CASE\n -- Handle regular columns\n WHEN a.attname IS NOT NULL THEN\n json_build_object('name', a.attname, 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n -- Handle expressions\n ELSE\n json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n END\n ORDER BY array_position(ix.indkey, k.attnum)\n ) AS index_columns\n FROM\n pg_class t\n LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table\n JOIN pg_index ix ON t.oid = ix.indrelid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON i.relam = am.oid\n LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true\n LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid\n LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]\n JOIN pg_namespace n ON t.relnamespace = n.oid\n WHERE\n n.nspname not like 'pg_%' and\n n.nspname <> 'information_schema'\n GROUP BY\n n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique\n ORDER BY\n COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection\n `);\n return indexes;\n }\n}\n\nexport type ColumnMetadata = {\n columnName: string;\n dataType: string;\n isNullable: boolean;\n stats: ColumnStats | null;\n};\n\ntype ColumnStats = {\n stainherit: boolean;\n stanullfrac: number;\n stawidth: number;\n stadistinct: number;\n stakind1: number;\n stakind2: number;\n stakind3: number;\n stakind4: number;\n stakind5: number;\n staop1: number;\n staop2: number;\n staop3: number;\n staop4: number;\n staop5: number;\n stacoll1: number;\n stacoll2: number;\n stacoll3: number;\n stacoll4: number;\n stacoll5: number;\n stanumbers1: number;\n stanumbers2: number;\n stanumbers3: number;\n stanumbers4: number;\n stanumbers5: number;\n};\n\nexport type TableMetadata = {\n tableName: string;\n schemaName: string;\n reltuples: number;\n relpages: number;\n relallvisible: number;\n relallfrozen?: number;\n columns: ColumnMetadata[];\n};\n\ntype TableName = string;\nexport type TableStats = {\n tupleEstimate: bigint;\n pageCount: number;\n};\n\nexport type SerializeResult = {\n schema: TableMetadata[];\n serialized: string;\n sampledRecords: Record<TableName, number>;\n};\n\nexport type IndexOrder = \"ASC\" | \"DESC\";\n\nexport type IndexedTable = {\n index_columns: Array<{ name: string; order: IndexOrder; opclass?: string }>;\n is_primary: boolean;\n is_unique: boolean;\n index_name: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n index_type: \"btree\" | \"gin\" | (string & {});\n // this is always public\n schema_name: string;\n table_name: string;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statistics.cjs","names":["z","PgIdentifier"],"sources":["../../src/optimizer/statistics.ts"],"sourcesContent":["import { gray } from \"colorette\";\nimport dedent from \"dedent\";\nimport { z } from \"zod\";\nimport type {\n Postgres,\n PostgresTransaction,\n PostgresVersion,\n} from \"../sql/database.ts\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexToCreate } from \"./genalgo.js\";\n\ntype StaValueKind = \"real\" | \"text\" | \"boolean\" | null;\n\nexport type Path = string;\n\nexport const StatisticsSource = z.union([\n z.object({\n kind: z.literal(\"path\"),\n path: z.string().min(1),\n }),\n z.object({\n kind: z.literal(\"inline\"),\n }),\n]);\n\nexport const ExportedStatsStatistics = z.object({\n stawidth: z.number(),\n stainherit: z.boolean().default(false),\n // 0 representing unknown\n stadistinct: z.number(),\n // this has no \"nullable\" state\n stanullfrac: z.number(),\n stakind1: z.number().min(0),\n stakind2: z.number().min(0),\n stakind3: z.number().min(0),\n stakind4: z.number().min(0),\n stakind5: z.number().min(0),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n // theoretically... this could only be strings and numbers\n // but we don't have a crystal ball\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n});\n\nexport const ExportedStatsColumns = z.object({\n columnName: z.string(),\n attlen: z.number().nullable(),\n dataType: z.string().optional(),\n stats: ExportedStatsStatistics.nullable(),\n});\n\nexport const ExportedStatsIndex = z.object({\n indexName: z.string(),\n amname: z.string().default(\"btree\"),\n relpages: z.number(),\n reltuples: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n fillfactor: z.number().default(90),\n columns: z.array(z.object({ attlen: z.number().nullable() })).default([]),\n});\n\n// This should match the output of the `_qd_dump_stats` function in the analyzer README.md\n// Need to make sure this is versioned to accept ALL potential outputs from every version of\n// dump functions we make public\nexport const ExportedStatsV1 = z.object({\n tableName: z.string(),\n schemaName: z.string(),\n // can be negative\n relpages: z.number(),\n // can be negative\n reltuples: z.number(),\n relallvisible: z.number(),\n // only postgres 18+\n relallfrozen: z.number().optional(),\n columns: z.array(ExportedStatsColumns).default([]),\n indexes: z.array(ExportedStatsIndex),\n});\n\nexport const ExportedStats = z.union([ExportedStatsV1]);\n\nexport type ExportedStats = z.infer<typeof ExportedStats>;\n\nexport const StatisticsMode = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"fromAssumption\"),\n reltuples: z.number().min(0),\n }),\n z.object({\n kind: z.literal(\"fromStatisticsExport\"),\n stats: z.array(ExportedStats),\n source: StatisticsSource,\n }),\n]);\n\nexport type StatisticsMode = z.infer<typeof StatisticsMode>;\n\nexport const ComputedColumnStats = z.object({\n schema_name: z.string(),\n table_name: z.string(),\n column_name: z.string(),\n data_type: z.string().optional(),\n stainherit: z.boolean(),\n stanullfrac: z.number(),\n stawidth: z.number(),\n stadistinct: z.number(),\n stakind1: z.number(),\n stakind2: z.number(),\n stakind3: z.number(),\n stakind4: z.number(),\n stakind5: z.number(),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n _value_type1: z.string().nullable(),\n _value_type2: z.string().nullable(),\n _value_type3: z.string().nullable(),\n _value_type4: z.string().nullable(),\n _value_type5: z.string().nullable(),\n});\n\nexport type ComputedColumnStats = z.infer<typeof ComputedColumnStats>;\n\nexport const ComputedReltuples = z.object({\n relname: z.string(),\n schema_name: z.string(),\n reltuples: z.number(),\n relpages: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n});\n\nexport type ComputedReltuples = z.infer<typeof ComputedReltuples>;\n\nexport const ComputedStats = z.object({\n columnStats: z.array(ComputedColumnStats),\n reltuples: z.array(ComputedReltuples),\n});\n\nexport type ComputedStats = z.infer<typeof ComputedStats>;\n\nconst DEFAULT_RELTUPLES = 10_000_000;\nconst DEFAULT_RELPAGES = 1;\n// it's _very_ rare that the default page size is ever changed\nconst DEFAULT_PAGE_SIZE = 2 ** 13;\n\nfunction estimateStawidth(col: { attlen?: number | null }): number {\n return col.attlen ?? 32;\n}\n\nfunction estimateRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n): number {\n const rowWidth =\n // 23 byte tuple header + 4 bytes alignment/null bitmap\n columns.reduce((sum, col) => sum + estimateStawidth(col), 0) + 27;\n return Math.ceil((reltuples * rowWidth) / DEFAULT_PAGE_SIZE);\n}\n\nfunction estimateIndexRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n fillfactor: number,\n amname: string,\n tableRelpages: number,\n): number {\n if (amname === \"gin\") {\n // GIN has an inverted structure; distinct element counts per row are unknown\n // without real data, so fall back to a ratio of the table page count\n return Math.ceil(tableRelpages * 0.3);\n }\n // 16 bytes btree entry overhead per key in addition to the key width\n const keyWidth = columns.reduce(\n (sum, col) => sum + estimateStawidth(col) + 16,\n 0,\n );\n return Math.ceil((reltuples * keyWidth) / DEFAULT_PAGE_SIZE / fillfactor);\n}\n\nexport const DUMP_STATS_SQL = dedent`\n WITH table_columns AS (\n SELECT\n cl.relname,\n n.nspname,\n cl.reltuples,\n cl.relpages,\n cl.relallvisible,\n -- cl.relallfrozen,\n json_agg(\n json_build_object(\n 'columnName', a.attname,\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END,\n 'dataType', t.typname,\n 'stats', (\n SELECT json_build_object(\n 'starelid', s.starelid,\n 'staattnum', s.staattnum,\n 'stanullfrac', s.stanullfrac,\n 'stawidth', s.stawidth,\n 'stadistinct', s.stadistinct,\n 'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,\n 'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,\n 'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,\n 'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,\n 'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,\n 'stavalues1', s.stavalues1,\n 'stavalues2', s.stavalues2,\n 'stavalues3', s.stavalues3,\n 'stavalues4', s.stavalues4,\n 'stavalues5', s.stavalues5\n )\n FROM pg_statistic s\n WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum\n )\n )\n ORDER BY a.attnum\n ) AS columns\n FROM pg_class cl\n JOIN pg_namespace n ON n.oid = cl.relnamespace\n JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attnum > 0 AND NOT a.attisdropped\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE cl.relkind = 'r'\n AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'tiger', 'tiger_data', 'topology')\n AND cl.relname NOT IN ('pg_stat_statements', 'pg_stat_statements_info')\n GROUP BY cl.relname, n.nspname, cl.reltuples, cl.relpages, cl.relallvisible\n ),\n table_indexes AS (\n SELECT\n t.relname AS table_name,\n json_agg(\n json_build_object(\n 'indexName', i.relname,\n 'amname', am.amname,\n 'reltuples', i.reltuples,\n 'relpages', i.relpages,\n 'relallvisible', i.relallvisible,\n -- 'relallfrozen', i.relallfrozen,\n 'fillfactor', COALESCE(\n (\n SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1\n ),\n 90\n ),\n 'columns', COALESCE(\n (\n SELECT json_agg(json_build_object(\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END\n ) ORDER BY col_pos.ord)\n FROM unnest(ix.indkey) WITH ORDINALITY AS col_pos(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = col_pos.attnum\n WHERE col_pos.attnum > 0\n ),\n '[]'::json\n )\n )\n ) AS indexes\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON am.oid = i.relam\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE t.relname NOT LIKE 'pg_%'\n AND n.nspname <> 'information_schema'\n AND n.nspname NOT IN ('tiger', 'tiger_data', 'topology')\n GROUP BY t.relname\n )\n SELECT json_agg(\n json_build_object(\n 'tableName', tc.relname,\n 'schemaName', tc.nspname,\n 'reltuples', tc.reltuples,\n 'relpages', tc.relpages,\n 'relallvisible', tc.relallvisible,\n -- 'relallfrozen', tc.relallfrozen,\n 'columns', COALESCE(tc.columns, '[]'::json),\n 'indexes', COALESCE(ti.indexes, '[]'::json)\n )\n )\n FROM table_columns tc\n LEFT JOIN table_indexes ti\n ON ti.table_name = tc.relname\n`;\n\nexport class Statistics {\n readonly mode: StatisticsMode;\n readonly computedStats: ComputedStats;\n private readonly exportedMetadata: ExportedStats[] | undefined;\n private additionalIndexes: IndexToCreate[] = [];\n // preventing accidental internal mutations\n static readonly defaultStatsMode: StatisticsMode = Object.freeze({\n kind: \"fromAssumption\",\n reltuples: DEFAULT_RELTUPLES,\n });\n constructor(\n private readonly db: Postgres,\n public readonly postgresVersion: PostgresVersion,\n public readonly ownMetadata: ExportedStats[],\n statsMode: StatisticsMode,\n ) {\n if (statsMode) {\n this.mode = statsMode;\n if (statsMode.kind === \"fromStatisticsExport\") {\n this.exportedMetadata = statsMode.stats;\n }\n } else {\n this.mode = Statistics.defaultStatsMode;\n }\n this.computedStats = this.buildComputedStats();\n }\n\n setAdditionalIndexes(additionalIndexes: IndexToCreate[]): void {\n this.additionalIndexes = additionalIndexes;\n }\n\n private buildComputedStats(): ComputedStats {\n const columnStats: ComputedColumnStats[] = [];\n const reltuples: ComputedReltuples[] = [];\n\n for (const table of this.ownMetadata) {\n const targetTable = this.exportedMetadata?.find(\n (m) =>\n m.tableName === table.tableName && m.schemaName === table.schemaName,\n );\n const target = targetTable?.columns ?? table.columns;\n\n for (const column of target) {\n const { stats } = column;\n if (!stats || this.mode.kind === \"fromAssumption\") {\n const stawidth = stats?.stawidth || estimateStawidth(column);\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: false,\n stanullfrac: 0.04,\n stawidth,\n stadistinct: -0.9,\n stakind1: 0,\n stakind2: 0,\n stakind3: 3,\n stakind4: 0,\n stakind5: 0,\n stacoll1: \"0\",\n stacoll2: \"0\",\n stacoll3: \"0\",\n stacoll4: \"0\",\n stacoll5: \"0\",\n staop1: \"0\",\n staop2: \"0\",\n staop3: \"0\",\n staop4: \"0\",\n staop5: \"0\",\n stanumbers1: null,\n stanumbers2: null,\n stanumbers3: [0.9],\n stanumbers4: null,\n stanumbers5: null,\n stavalues1: null,\n stavalues2: null,\n stavalues3: null,\n stavalues4: null,\n stavalues5: null,\n _value_type1: \"real\",\n _value_type2: \"real\",\n _value_type3: \"real\",\n _value_type4: \"real\",\n _value_type5: \"real\",\n });\n } else {\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: stats.stainherit ?? false,\n stanullfrac: stats.stanullfrac,\n stawidth: stats.stawidth,\n stadistinct: stats.stadistinct,\n stakind1: stats.stakind1,\n stakind2: stats.stakind2,\n stakind3: stats.stakind3,\n stakind4: stats.stakind4,\n stakind5: stats.stakind5,\n staop1: stats.staop1,\n staop2: stats.staop2,\n staop3: stats.staop3,\n staop4: stats.staop4,\n staop5: stats.staop5,\n stacoll1: stats.stacoll1,\n stacoll2: stats.stacoll2,\n stacoll3: stats.stacoll3,\n stacoll4: stats.stacoll4,\n stacoll5: stats.stacoll5,\n stanumbers1: stats.stanumbers1,\n stanumbers2: stats.stanumbers2,\n stanumbers3: stats.stanumbers3,\n stanumbers4: stats.stanumbers4,\n stanumbers5: stats.stanumbers5,\n stavalues1: Statistics.safeStavalues(stats.stavalues1),\n stavalues2: Statistics.safeStavalues(stats.stavalues2),\n stavalues3: Statistics.safeStavalues(stats.stavalues3),\n stavalues4: Statistics.safeStavalues(stats.stavalues4),\n stavalues5: Statistics.safeStavalues(stats.stavalues5),\n _value_type1: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues1),\n ),\n _value_type2: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues2),\n ),\n _value_type3: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues3),\n ),\n _value_type4: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues4),\n ),\n _value_type5: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues5),\n ),\n });\n }\n }\n\n let tableReltuples: number;\n let tableRelpages: number;\n let relallvisible = 0;\n let relallfrozen: number | undefined;\n\n if (this.mode.kind === \"fromAssumption\") {\n tableReltuples = this.mode.reltuples;\n tableRelpages = estimateRelpages(tableReltuples, table.columns);\n } else if (targetTable) {\n tableReltuples = targetTable.reltuples;\n tableRelpages = targetTable.relpages;\n relallvisible = targetTable.relallvisible;\n relallfrozen = targetTable.relallfrozen;\n } else {\n tableReltuples = DEFAULT_RELTUPLES;\n tableRelpages = DEFAULT_RELPAGES;\n }\n\n reltuples.push({\n relname: table.tableName,\n schema_name: table.schemaName,\n reltuples: tableReltuples,\n relpages: tableRelpages,\n relallfrozen,\n relallvisible,\n });\n\n if (this.mode.kind === \"fromAssumption\") {\n for (const index of table.indexes) {\n const indexRelpages = estimateIndexRelpages(\n this.mode.reltuples,\n index.columns,\n index.fillfactor / 100,\n index.amname,\n tableRelpages,\n );\n reltuples.push({\n relname: index.indexName,\n schema_name: table.schemaName,\n reltuples: this.mode.reltuples,\n relpages: indexRelpages,\n relallfrozen: 0,\n relallvisible: indexRelpages,\n });\n }\n } else if (targetTable) {\n for (const index of targetTable.indexes) {\n reltuples.push({\n relname: index.indexName,\n schema_name: targetTable.schemaName,\n reltuples: index.reltuples,\n relpages: index.relpages,\n relallfrozen: index.relallfrozen,\n relallvisible: index.relallvisible,\n });\n }\n }\n }\n\n return { columnStats, reltuples };\n }\n\n static statsModeFromAssumption({\n reltuples,\n }: {\n reltuples: number;\n }): StatisticsMode {\n return {\n kind: \"fromAssumption\",\n reltuples,\n };\n }\n\n /**\n * Create a statistic mode from stats exported from another database\n **/\n static statsModeFromExport(stats: ExportedStats[]): StatisticsMode {\n return {\n kind: \"fromStatisticsExport\",\n source: { kind: \"inline\" },\n stats,\n };\n }\n\n static async fromPostgres(\n db: Postgres,\n statsMode: StatisticsMode,\n ): Promise<Statistics> {\n const version = await db.serverNum();\n const ownStats = await Statistics.dumpStats(db, version);\n return new Statistics(db, version, ownStats, statsMode);\n }\n\n restoreStats(tx: PostgresTransaction) {\n // if (this.postgresVersion < \"180000\") {\n return this.restoreStats17(tx);\n // }\n // return this.restoreStats18(tx);\n }\n\n approximateTotalRows() {\n if (!this.exportedMetadata) {\n return 0;\n }\n let totalRows = 0;\n for (const table of this.exportedMetadata) {\n totalRows += table.reltuples;\n }\n return totalRows;\n }\n\n /**\n * We have to cast stavaluesN to the correct type\n * This derives that type for us so it can be used in `array_in`\n */\n private stavalueKind(values: unknown[] | null): StaValueKind {\n if (!values || values.length === 0) {\n return null;\n }\n const [elem] = values;\n if (typeof elem === \"number\") {\n return \"real\";\n } else if (typeof elem === \"boolean\") {\n return \"boolean\";\n }\n // is everything else a text? What about strinfied dates?\n // we might need column metadata access here if we do\n return \"text\";\n }\n\n /**\n * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays\n * for columns with array types (e.g. text[], int4[]). These create\n * multidimensional arrays that can be \"ragged\" (sub-arrays with different\n * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional\n * arrays from JSON, so we need to drop these values.\n */\n private static safeStavalues(values: unknown[] | null): unknown[] | null {\n if (!values || values.length === 0) return values;\n if (values.some((v) => Array.isArray(v))) {\n console.warn(\"Discarding ragged multidimensional stavalues array\");\n return null;\n }\n return values;\n }\n\n /**\n * When inserting fake stats for existing tables and indexes, we also need to\n * account for data on newly created indexes by the optimizer.\n *\n * However we assume that index reltuples = table reltuples.\n * Meaning this logic is going to be a little off for partial indexes\n * or posting lists for duplicate values in btrees.\n * Meaning the deduplication that happens in pg side\n * 4 reltuples [(2, 1),(2, 9),(2, 4),(2, 8)] -> 1 reltuple [2, (1, 9, 4, 8)]\n * does not get accounted for here.\n */\n private async getAdditionalIndexReltuples(\n tx: PostgresTransaction,\n ): Promise<ComputedReltuples[]> {\n if (this.additionalIndexes.length === 0) {\n return [];\n }\n\n type ColumnAttlenRow = {\n index_name: string;\n schema_name: string;\n table_name: string;\n column_name: string;\n attlen: number | null;\n fillfactor: number;\n };\n\n const indexNames = this.additionalIndexes.map((idx) => idx.name.toString());\n const rows = await tx\n .exec<ColumnAttlenRow>(\n dedent`\n SELECT\n i.relname AS index_name,\n n.nspname AS schema_name,\n t.relname AS table_name,\n a.attname AS column_name,\n CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END AS attlen,\n COALESCE(\n (SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1),\n 90\n ) AS fillfactor\n FROM pg_class i\n JOIN pg_index ix ON ix.indexrelid = i.oid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN unnest(ix.indkey) AS k(attnum) ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE i.relname IN (SELECT jsonb_array_elements_text($1::jsonb))\n AND k.attnum > 0\n `,\n [indexNames],\n )\n .catch((err) => {\n console.error(\n \"Something went wrong querying additional index column metadata\",\n );\n console.error(err);\n return [];\n });\n\n const attlenByKey = new Map<string, number | null>();\n const fillfactorByIndex = new Map<string, number>();\n for (const row of rows) {\n attlenByKey.set(\n `${row.schema_name}.${row.table_name}.${row.column_name}`,\n row.attlen,\n );\n fillfactorByIndex.set(row.index_name, row.fillfactor);\n }\n\n return this.additionalIndexes.flatMap((idx) => {\n const tableStats = this.computedStats.reltuples.find((r) => {\n // making sure to follow normalization rules\n const sourceSchema = PgIdentifier.fromString(r.schema_name).toString();\n const sourceTable = PgIdentifier.fromString(r.relname).toString();\n\n const targetSchema = PgIdentifier.fromString(idx.schema).toString();\n const targetTable = PgIdentifier.fromString(idx.table).toString();\n return sourceSchema === targetSchema && sourceTable === targetTable;\n });\n\n if (!tableStats) {\n return [];\n }\n const columns = idx.columns.map((col) => ({\n attlen:\n attlenByKey.get(`${idx.schema}.${idx.table}.${col.column}`) ?? null,\n }));\n const fillfactor = fillfactorByIndex.get(idx.name.toString()) ?? 90;\n const amname = idx.indexMethod ?? \"btree\";\n const relpages = estimateIndexRelpages(\n tableStats.reltuples,\n columns,\n fillfactor / 100,\n amname,\n tableStats.relpages,\n );\n\n return [\n {\n relname: idx.name.toString(),\n schema_name: idx.schema,\n reltuples: tableStats.reltuples,\n relpages,\n relallvisible: relpages,\n relallfrozen: 0,\n },\n ];\n });\n }\n\n private async restoreStats17(tx: PostgresTransaction) {\n const warnings = {\n tablesNotInExports: [] as string[],\n tablesNotInTest: [] as string[],\n tableNotAnalyzed: [] as string[],\n statsMissing: [] as {\n statistic: string;\n table: string;\n schema: string;\n column: string;\n }[],\n };\n\n const processedTables = new Set<string>(\n this.ownMetadata.map((t) => `${t.schemaName}.${t.tableName}`),\n );\n\n const columnStatsUpdatePromise = tx\n .exec(Statistics.columnStatsSQL, [this.computedStats.columnStats])\n .catch((err) => {\n console.error(\"Something wrong wrong updating column stats\");\n console.error(err);\n throw err;\n });\n\n /**\n * Postgres has 5 different slots for storing statistics per column and a potentially unlimited\n * number of statistic types to choose from. Each code in `stakindN` can mean different things.\n * Some statistics are just numerical values such as `n_distinct` and `correlation`, meaning\n * they're only derived from `stanumbersN` and the value of `stanumbersN` is never read.\n * Others take advantage of the `stavaluesN` columns which use `anyarray` type to store\n * concrete values internally for things like histogram bounds.\n * Unfortunately we cannot change anyarrays without a C extension.\n *\n * (1) = most common values\n * (2) = scalar histogram\n * (3) = correlation <- can change\n * (4) = most common elements\n * (5) = distinct elem count histogram <- can change\n * (6) = length histogram (?) These don't appear in pg_stats\n * (7) = bounds histogram (?) These don't appear in pg_stats\n * (N) = potentially many more kinds of statistics. But postgres <=18 only uses these 7.\n *\n * What we're doing here is setting ANY statistic we cannot directly control\n * (anything that relies on stavaluesN) to 0 to make sure the planner isn't influenced by what\n * what the db collected from the test data.\n * Because we do our tests with `generic_plan` it seems it's already unlikely that the planner will be\n * using things like common values or histogram bounds to make the planning decisions we care about.\n * This is a just in case.\n */\n const reltuplesQuery = dedent`\n update pg_class p\n set reltuples = v.reltuples,\n relpages = v.relpages,\n -- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,\n relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end\n from jsonb_to_recordset($1::jsonb)\n as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)\n where p.relname = v.relname\n and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)\n returning p.relname, p.relnamespace, p.reltuples, p.relpages;\n `;\n\n const additionalIndexReltuples = await this.getAdditionalIndexReltuples(tx);\n\n const reltuplesPromise = tx\n .exec(reltuplesQuery, [\n [...this.computedStats.reltuples, ...additionalIndexReltuples],\n ])\n .catch((err) => {\n console.error(\"Something went wrong updating reltuples/relpages\");\n console.error(err);\n return err;\n });\n\n if (this.exportedMetadata) {\n for (const table of this.exportedMetadata) {\n const tableExists = processedTables.has(\n `${table.schemaName}.${table.tableName}`,\n );\n if (tableExists && table.reltuples === -1) {\n console.warn(\n `Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`,\n );\n warnings.tableNotAnalyzed.push(\n `${table.schemaName}.${table.tableName}`,\n );\n }\n if (tableExists) {\n continue;\n }\n warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);\n }\n }\n const [statsUpdates, reltuplesUpdates] = await Promise.all([\n columnStatsUpdatePromise,\n reltuplesPromise,\n ]);\n const updatedColumnsProperly = statsUpdates\n ? statsUpdates.length === this.computedStats.columnStats.length\n : true;\n if (!updatedColumnsProperly) {\n console.error(`Did not update expected column stats`);\n }\n if (reltuplesUpdates.length !== this.computedStats.reltuples.length) {\n console.error(`Did not update expected reltuples/relpages`);\n }\n return warnings;\n }\n\n private static readonly columnStatsSQL = dedent`\n WITH input AS (\n SELECT\n c.oid AS starelid,\n a.attnum AS staattnum,\n v.stainherit,\n v.stanullfrac,\n v.stawidth,\n v.stadistinct,\n v.stakind1,\n v.stakind2,\n v.stakind3,\n v.stakind4,\n v.stakind5,\n v.staop1,\n v.staop2,\n v.staop3,\n v.staop4,\n v.staop5,\n v.stacoll1,\n v.stacoll2,\n v.stacoll3,\n v.stacoll4,\n v.stacoll5,\n v.stanumbers1,\n v.stanumbers2,\n v.stanumbers3,\n v.stanumbers4,\n v.stanumbers5,\n v.stavalues1,\n v.stavalues2,\n v.stavalues3,\n v.stavalues4,\n v.stavalues5,\n _value_type1,\n _value_type2,\n _value_type3,\n _value_type4,\n _value_type5\n FROM jsonb_to_recordset($1::jsonb) AS v(\n schema_name text,\n table_name text,\n column_name text,\n stainherit boolean,\n stanullfrac real,\n stawidth integer,\n stadistinct real,\n stakind1 real,\n stakind2 real,\n stakind3 real,\n stakind4 real,\n stakind5 real,\n staop1 oid,\n staop2 oid,\n staop3 oid,\n staop4 oid,\n staop5 oid,\n stacoll1 oid,\n stacoll2 oid,\n stacoll3 oid,\n stacoll4 oid,\n stacoll5 oid,\n stanumbers1 real[],\n stanumbers2 real[],\n stanumbers3 real[],\n stanumbers4 real[],\n stanumbers5 real[],\n stavalues1 text[],\n stavalues2 text[],\n stavalues3 text[],\n stavalues4 text[],\n stavalues5 text[],\n _value_type1 text,\n _value_type2 text,\n _value_type3 text,\n _value_type4 text,\n _value_type5 text\n )\n JOIN pg_class c ON c.relname = v.table_name\n JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name\n ),\n updated AS (\n UPDATE pg_statistic s\n SET\n stanullfrac = i.stanullfrac,\n stawidth = i.stawidth,\n stadistinct = i.stadistinct,\n stakind1 = i.stakind1,\n stakind2 = i.stakind2,\n stakind3 = i.stakind3,\n stakind4 = i.stakind4,\n stakind5 = i.stakind5,\n staop1 = i.staop1,\n staop2 = i.staop2,\n staop3 = i.staop3,\n staop4 = i.staop4,\n staop5 = i.staop5,\n stacoll1 = i.stacoll1,\n stacoll2 = i.stacoll2,\n stacoll3 = i.stacoll3,\n stacoll4 = i.stacoll4,\n stacoll5 = i.stacoll5,\n stanumbers1 = i.stanumbers1,\n stanumbers2 = i.stanumbers2,\n stanumbers3 = i.stanumbers3,\n stanumbers4 = i.stanumbers4,\n stanumbers5 = i.stanumbers5,\n stavalues1 = case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n stavalues2 = case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n stavalues3 = case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n stavalues4 = case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n stavalues5 = case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- stavalues1 = i.stavalues1,\n -- stavalues2 = i.stavalues2,\n -- stavalues3 = i.stavalues3,\n -- stavalues4 = i.stavalues4,\n -- stavalues5 = i.stavalues5\n FROM input i\n WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit\n RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5\n ),\n inserted as (\n INSERT INTO pg_statistic (\n starelid, staattnum, stainherit,\n stanullfrac, stawidth, stadistinct,\n stakind1, stakind2, stakind3, stakind4, stakind5,\n staop1, staop2, staop3, staop4, staop5,\n stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,\n stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,\n stavalues1, stavalues2, stavalues3, stavalues4, stavalues5\n )\n SELECT\n i.starelid, i.staattnum, i.stainherit,\n i.stanullfrac, i.stawidth, i.stadistinct,\n i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,\n i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,\n i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,\n i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,\n -- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,\n case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5\n FROM input i\n LEFT JOIN updated u\n ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit\n WHERE u.starelid IS NULL\n returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5\n )\n select * from updated union all (select * from inserted); -- @qd_introspection`;\n\n static async dumpStats(\n db: PostgresTransaction,\n postgresVersion: PostgresVersion,\n ): Promise<ExportedStats[]> {\n console.log(`dumping stats for postgres ${gray(postgresVersion)}`);\n const stats = await db.exec<{ json_agg: ExportedStats[] }>(DUMP_STATS_SQL);\n const out = z.array(ExportedStats).parse(stats[0].json_agg ?? []);\n return out;\n }\n\n /**\n * Returns all indexes in the database.\n * ONLY handles regular btree indexes\n */\n async getExistingIndexes(): Promise<IndexedTable[]> {\n const indexes = await this.db.exec<IndexedTable>(`\n WITH partitioned_tables AS (\n SELECT\n inhparent::regclass AS parent_table,\n inhrelid::regclass AS partition_table\n FROM\n pg_inherits\n )\n SELECT\n n.nspname AS schema_name,\n COALESCE(pt.parent_table::text, t.relname) AS table_name,\n i.relname AS index_name,\n ix.indisprimary as is_primary,\n ix.indisunique as is_unique,\n am.amname AS index_type,\n array_agg(\n CASE\n -- Handle regular columns\n WHEN a.attname IS NOT NULL THEN\n json_build_object('name', a.attname, 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n -- Handle expressions\n ELSE\n json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n END\n ORDER BY array_position(ix.indkey, k.attnum)\n ) AS index_columns\n FROM\n pg_class t\n LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table\n JOIN pg_index ix ON t.oid = ix.indrelid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON i.relam = am.oid\n LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true\n LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid\n LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]\n JOIN pg_namespace n ON t.relnamespace = n.oid\n WHERE\n n.nspname not like 'pg_%' and\n n.nspname <> 'information_schema'\n GROUP BY\n n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique\n ORDER BY\n COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection\n `);\n return indexes;\n }\n}\n\nexport type ColumnMetadata = {\n columnName: string;\n dataType: string;\n isNullable: boolean;\n stats: ColumnStats | null;\n};\n\ntype ColumnStats = {\n stainherit: boolean;\n stanullfrac: number;\n stawidth: number;\n stadistinct: number;\n stakind1: number;\n stakind2: number;\n stakind3: number;\n stakind4: number;\n stakind5: number;\n staop1: number;\n staop2: number;\n staop3: number;\n staop4: number;\n staop5: number;\n stacoll1: number;\n stacoll2: number;\n stacoll3: number;\n stacoll4: number;\n stacoll5: number;\n stanumbers1: number;\n stanumbers2: number;\n stanumbers3: number;\n stanumbers4: number;\n stanumbers5: number;\n};\n\nexport type TableMetadata = {\n tableName: string;\n schemaName: string;\n reltuples: number;\n relpages: number;\n relallvisible: number;\n relallfrozen?: number;\n columns: ColumnMetadata[];\n};\n\ntype TableName = string;\nexport type TableStats = {\n tupleEstimate: bigint;\n pageCount: number;\n};\n\nexport type SerializeResult = {\n schema: TableMetadata[];\n serialized: string;\n sampledRecords: Record<TableName, number>;\n};\n\nexport type IndexOrder = \"ASC\" | \"DESC\";\n\nexport type IndexedTable = {\n index_columns: Array<{ name: string; order: IndexOrder; opclass?: string }>;\n is_primary: boolean;\n is_unique: boolean;\n index_name: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n index_type: \"btree\" | \"gin\" | (string & {});\n // this is always public\n schema_name: string;\n table_name: string;\n};\n"],"mappings":"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"}
|
|
@@ -604,7 +604,7 @@ var Statistics = class Statistics {
|
|
|
604
604
|
static async dumpStats(db, postgresVersion) {
|
|
605
605
|
console.log(`dumping stats for postgres ${gray(postgresVersion)}`);
|
|
606
606
|
const stats = await db.exec(DUMP_STATS_SQL);
|
|
607
|
-
return z.array(ExportedStats).parse(stats[0].json_agg);
|
|
607
|
+
return z.array(ExportedStats).parse(stats[0].json_agg ?? []);
|
|
608
608
|
}
|
|
609
609
|
/**
|
|
610
610
|
* Returns all indexes in the database.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statistics.mjs","names":[],"sources":["../../src/optimizer/statistics.ts"],"sourcesContent":["import { gray } from \"colorette\";\nimport dedent from \"dedent\";\nimport { z } from \"zod\";\nimport type {\n Postgres,\n PostgresTransaction,\n PostgresVersion,\n} from \"../sql/database.ts\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexToCreate } from \"./genalgo.js\";\n\ntype StaValueKind = \"real\" | \"text\" | \"boolean\" | null;\n\nexport type Path = string;\n\nexport const StatisticsSource = z.union([\n z.object({\n kind: z.literal(\"path\"),\n path: z.string().min(1),\n }),\n z.object({\n kind: z.literal(\"inline\"),\n }),\n]);\n\nexport const ExportedStatsStatistics = z.object({\n stawidth: z.number(),\n stainherit: z.boolean().default(false),\n // 0 representing unknown\n stadistinct: z.number(),\n // this has no \"nullable\" state\n stanullfrac: z.number(),\n stakind1: z.number().min(0),\n stakind2: z.number().min(0),\n stakind3: z.number().min(0),\n stakind4: z.number().min(0),\n stakind5: z.number().min(0),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n // theoretically... this could only be strings and numbers\n // but we don't have a crystal ball\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n});\n\nexport const ExportedStatsColumns = z.object({\n columnName: z.string(),\n attlen: z.number().nullable(),\n dataType: z.string().optional(),\n stats: ExportedStatsStatistics.nullable(),\n});\n\nexport const ExportedStatsIndex = z.object({\n indexName: z.string(),\n amname: z.string().default(\"btree\"),\n relpages: z.number(),\n reltuples: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n fillfactor: z.number().default(90),\n columns: z.array(z.object({ attlen: z.number().nullable() })).default([]),\n});\n\n// This should match the output of the `_qd_dump_stats` function in the analyzer README.md\n// Need to make sure this is versioned to accept ALL potential outputs from every version of\n// dump functions we make public\nexport const ExportedStatsV1 = z.object({\n tableName: z.string(),\n schemaName: z.string(),\n // can be negative\n relpages: z.number(),\n // can be negative\n reltuples: z.number(),\n relallvisible: z.number(),\n // only postgres 18+\n relallfrozen: z.number().optional(),\n columns: z.array(ExportedStatsColumns).default([]),\n indexes: z.array(ExportedStatsIndex),\n});\n\nexport const ExportedStats = z.union([ExportedStatsV1]);\n\nexport type ExportedStats = z.infer<typeof ExportedStats>;\n\nexport const StatisticsMode = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"fromAssumption\"),\n reltuples: z.number().min(0),\n }),\n z.object({\n kind: z.literal(\"fromStatisticsExport\"),\n stats: z.array(ExportedStats),\n source: StatisticsSource,\n }),\n]);\n\nexport type StatisticsMode = z.infer<typeof StatisticsMode>;\n\nexport const ComputedColumnStats = z.object({\n schema_name: z.string(),\n table_name: z.string(),\n column_name: z.string(),\n data_type: z.string().optional(),\n stainherit: z.boolean(),\n stanullfrac: z.number(),\n stawidth: z.number(),\n stadistinct: z.number(),\n stakind1: z.number(),\n stakind2: z.number(),\n stakind3: z.number(),\n stakind4: z.number(),\n stakind5: z.number(),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n _value_type1: z.string().nullable(),\n _value_type2: z.string().nullable(),\n _value_type3: z.string().nullable(),\n _value_type4: z.string().nullable(),\n _value_type5: z.string().nullable(),\n});\n\nexport type ComputedColumnStats = z.infer<typeof ComputedColumnStats>;\n\nexport const ComputedReltuples = z.object({\n relname: z.string(),\n schema_name: z.string(),\n reltuples: z.number(),\n relpages: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n});\n\nexport type ComputedReltuples = z.infer<typeof ComputedReltuples>;\n\nexport const ComputedStats = z.object({\n columnStats: z.array(ComputedColumnStats),\n reltuples: z.array(ComputedReltuples),\n});\n\nexport type ComputedStats = z.infer<typeof ComputedStats>;\n\nconst DEFAULT_RELTUPLES = 10_000_000;\nconst DEFAULT_RELPAGES = 1;\n// it's _very_ rare that the default page size is ever changed\nconst DEFAULT_PAGE_SIZE = 2 ** 13;\n\nfunction estimateStawidth(col: { attlen?: number | null }): number {\n return col.attlen ?? 32;\n}\n\nfunction estimateRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n): number {\n const rowWidth =\n // 23 byte tuple header + 4 bytes alignment/null bitmap\n columns.reduce((sum, col) => sum + estimateStawidth(col), 0) + 27;\n return Math.ceil((reltuples * rowWidth) / DEFAULT_PAGE_SIZE);\n}\n\nfunction estimateIndexRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n fillfactor: number,\n amname: string,\n tableRelpages: number,\n): number {\n if (amname === \"gin\") {\n // GIN has an inverted structure; distinct element counts per row are unknown\n // without real data, so fall back to a ratio of the table page count\n return Math.ceil(tableRelpages * 0.3);\n }\n // 16 bytes btree entry overhead per key in addition to the key width\n const keyWidth = columns.reduce(\n (sum, col) => sum + estimateStawidth(col) + 16,\n 0,\n );\n return Math.ceil((reltuples * keyWidth) / DEFAULT_PAGE_SIZE / fillfactor);\n}\n\nexport const DUMP_STATS_SQL = dedent`\n WITH table_columns AS (\n SELECT\n cl.relname,\n n.nspname,\n cl.reltuples,\n cl.relpages,\n cl.relallvisible,\n -- cl.relallfrozen,\n json_agg(\n json_build_object(\n 'columnName', a.attname,\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END,\n 'dataType', t.typname,\n 'stats', (\n SELECT json_build_object(\n 'starelid', s.starelid,\n 'staattnum', s.staattnum,\n 'stanullfrac', s.stanullfrac,\n 'stawidth', s.stawidth,\n 'stadistinct', s.stadistinct,\n 'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,\n 'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,\n 'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,\n 'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,\n 'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,\n 'stavalues1', s.stavalues1,\n 'stavalues2', s.stavalues2,\n 'stavalues3', s.stavalues3,\n 'stavalues4', s.stavalues4,\n 'stavalues5', s.stavalues5\n )\n FROM pg_statistic s\n WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum\n )\n )\n ORDER BY a.attnum\n ) AS columns\n FROM pg_class cl\n JOIN pg_namespace n ON n.oid = cl.relnamespace\n JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attnum > 0 AND NOT a.attisdropped\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE cl.relkind = 'r'\n AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'tiger', 'tiger_data', 'topology')\n AND cl.relname NOT IN ('pg_stat_statements', 'pg_stat_statements_info')\n GROUP BY cl.relname, n.nspname, cl.reltuples, cl.relpages, cl.relallvisible\n ),\n table_indexes AS (\n SELECT\n t.relname AS table_name,\n json_agg(\n json_build_object(\n 'indexName', i.relname,\n 'amname', am.amname,\n 'reltuples', i.reltuples,\n 'relpages', i.relpages,\n 'relallvisible', i.relallvisible,\n -- 'relallfrozen', i.relallfrozen,\n 'fillfactor', COALESCE(\n (\n SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1\n ),\n 90\n ),\n 'columns', COALESCE(\n (\n SELECT json_agg(json_build_object(\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END\n ) ORDER BY col_pos.ord)\n FROM unnest(ix.indkey) WITH ORDINALITY AS col_pos(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = col_pos.attnum\n WHERE col_pos.attnum > 0\n ),\n '[]'::json\n )\n )\n ) AS indexes\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON am.oid = i.relam\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE t.relname NOT LIKE 'pg_%'\n AND n.nspname <> 'information_schema'\n AND n.nspname NOT IN ('tiger', 'tiger_data', 'topology')\n GROUP BY t.relname\n )\n SELECT json_agg(\n json_build_object(\n 'tableName', tc.relname,\n 'schemaName', tc.nspname,\n 'reltuples', tc.reltuples,\n 'relpages', tc.relpages,\n 'relallvisible', tc.relallvisible,\n -- 'relallfrozen', tc.relallfrozen,\n 'columns', COALESCE(tc.columns, '[]'::json),\n 'indexes', COALESCE(ti.indexes, '[]'::json)\n )\n )\n FROM table_columns tc\n LEFT JOIN table_indexes ti\n ON ti.table_name = tc.relname\n`;\n\nexport class Statistics {\n readonly mode: StatisticsMode;\n readonly computedStats: ComputedStats;\n private readonly exportedMetadata: ExportedStats[] | undefined;\n private additionalIndexes: IndexToCreate[] = [];\n // preventing accidental internal mutations\n static readonly defaultStatsMode: StatisticsMode = Object.freeze({\n kind: \"fromAssumption\",\n reltuples: DEFAULT_RELTUPLES,\n });\n constructor(\n private readonly db: Postgres,\n public readonly postgresVersion: PostgresVersion,\n public readonly ownMetadata: ExportedStats[],\n statsMode: StatisticsMode,\n ) {\n if (statsMode) {\n this.mode = statsMode;\n if (statsMode.kind === \"fromStatisticsExport\") {\n this.exportedMetadata = statsMode.stats;\n }\n } else {\n this.mode = Statistics.defaultStatsMode;\n }\n this.computedStats = this.buildComputedStats();\n }\n\n setAdditionalIndexes(additionalIndexes: IndexToCreate[]): void {\n this.additionalIndexes = additionalIndexes;\n }\n\n private buildComputedStats(): ComputedStats {\n const columnStats: ComputedColumnStats[] = [];\n const reltuples: ComputedReltuples[] = [];\n\n for (const table of this.ownMetadata) {\n const targetTable = this.exportedMetadata?.find(\n (m) =>\n m.tableName === table.tableName && m.schemaName === table.schemaName,\n );\n const target = targetTable?.columns ?? table.columns;\n\n for (const column of target) {\n const { stats } = column;\n if (!stats || this.mode.kind === \"fromAssumption\") {\n const stawidth = stats?.stawidth || estimateStawidth(column);\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: false,\n stanullfrac: 0.04,\n stawidth,\n stadistinct: -0.9,\n stakind1: 0,\n stakind2: 0,\n stakind3: 3,\n stakind4: 0,\n stakind5: 0,\n stacoll1: \"0\",\n stacoll2: \"0\",\n stacoll3: \"0\",\n stacoll4: \"0\",\n stacoll5: \"0\",\n staop1: \"0\",\n staop2: \"0\",\n staop3: \"0\",\n staop4: \"0\",\n staop5: \"0\",\n stanumbers1: null,\n stanumbers2: null,\n stanumbers3: [0.9],\n stanumbers4: null,\n stanumbers5: null,\n stavalues1: null,\n stavalues2: null,\n stavalues3: null,\n stavalues4: null,\n stavalues5: null,\n _value_type1: \"real\",\n _value_type2: \"real\",\n _value_type3: \"real\",\n _value_type4: \"real\",\n _value_type5: \"real\",\n });\n } else {\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: stats.stainherit ?? false,\n stanullfrac: stats.stanullfrac,\n stawidth: stats.stawidth,\n stadistinct: stats.stadistinct,\n stakind1: stats.stakind1,\n stakind2: stats.stakind2,\n stakind3: stats.stakind3,\n stakind4: stats.stakind4,\n stakind5: stats.stakind5,\n staop1: stats.staop1,\n staop2: stats.staop2,\n staop3: stats.staop3,\n staop4: stats.staop4,\n staop5: stats.staop5,\n stacoll1: stats.stacoll1,\n stacoll2: stats.stacoll2,\n stacoll3: stats.stacoll3,\n stacoll4: stats.stacoll4,\n stacoll5: stats.stacoll5,\n stanumbers1: stats.stanumbers1,\n stanumbers2: stats.stanumbers2,\n stanumbers3: stats.stanumbers3,\n stanumbers4: stats.stanumbers4,\n stanumbers5: stats.stanumbers5,\n stavalues1: Statistics.safeStavalues(stats.stavalues1),\n stavalues2: Statistics.safeStavalues(stats.stavalues2),\n stavalues3: Statistics.safeStavalues(stats.stavalues3),\n stavalues4: Statistics.safeStavalues(stats.stavalues4),\n stavalues5: Statistics.safeStavalues(stats.stavalues5),\n _value_type1: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues1),\n ),\n _value_type2: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues2),\n ),\n _value_type3: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues3),\n ),\n _value_type4: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues4),\n ),\n _value_type5: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues5),\n ),\n });\n }\n }\n\n let tableReltuples: number;\n let tableRelpages: number;\n let relallvisible = 0;\n let relallfrozen: number | undefined;\n\n if (this.mode.kind === \"fromAssumption\") {\n tableReltuples = this.mode.reltuples;\n tableRelpages = estimateRelpages(tableReltuples, table.columns);\n } else if (targetTable) {\n tableReltuples = targetTable.reltuples;\n tableRelpages = targetTable.relpages;\n relallvisible = targetTable.relallvisible;\n relallfrozen = targetTable.relallfrozen;\n } else {\n tableReltuples = DEFAULT_RELTUPLES;\n tableRelpages = DEFAULT_RELPAGES;\n }\n\n reltuples.push({\n relname: table.tableName,\n schema_name: table.schemaName,\n reltuples: tableReltuples,\n relpages: tableRelpages,\n relallfrozen,\n relallvisible,\n });\n\n if (this.mode.kind === \"fromAssumption\") {\n for (const index of table.indexes) {\n const indexRelpages = estimateIndexRelpages(\n this.mode.reltuples,\n index.columns,\n index.fillfactor / 100,\n index.amname,\n tableRelpages,\n );\n reltuples.push({\n relname: index.indexName,\n schema_name: table.schemaName,\n reltuples: this.mode.reltuples,\n relpages: indexRelpages,\n relallfrozen: 0,\n relallvisible: indexRelpages,\n });\n }\n } else if (targetTable) {\n for (const index of targetTable.indexes) {\n reltuples.push({\n relname: index.indexName,\n schema_name: targetTable.schemaName,\n reltuples: index.reltuples,\n relpages: index.relpages,\n relallfrozen: index.relallfrozen,\n relallvisible: index.relallvisible,\n });\n }\n }\n }\n\n return { columnStats, reltuples };\n }\n\n static statsModeFromAssumption({\n reltuples,\n }: {\n reltuples: number;\n }): StatisticsMode {\n return {\n kind: \"fromAssumption\",\n reltuples,\n };\n }\n\n /**\n * Create a statistic mode from stats exported from another database\n **/\n static statsModeFromExport(stats: ExportedStats[]): StatisticsMode {\n return {\n kind: \"fromStatisticsExport\",\n source: { kind: \"inline\" },\n stats,\n };\n }\n\n static async fromPostgres(\n db: Postgres,\n statsMode: StatisticsMode,\n ): Promise<Statistics> {\n const version = await db.serverNum();\n const ownStats = await Statistics.dumpStats(db, version);\n return new Statistics(db, version, ownStats, statsMode);\n }\n\n restoreStats(tx: PostgresTransaction) {\n // if (this.postgresVersion < \"180000\") {\n return this.restoreStats17(tx);\n // }\n // return this.restoreStats18(tx);\n }\n\n approximateTotalRows() {\n if (!this.exportedMetadata) {\n return 0;\n }\n let totalRows = 0;\n for (const table of this.exportedMetadata) {\n totalRows += table.reltuples;\n }\n return totalRows;\n }\n\n /**\n * We have to cast stavaluesN to the correct type\n * This derives that type for us so it can be used in `array_in`\n */\n private stavalueKind(values: unknown[] | null): StaValueKind {\n if (!values || values.length === 0) {\n return null;\n }\n const [elem] = values;\n if (typeof elem === \"number\") {\n return \"real\";\n } else if (typeof elem === \"boolean\") {\n return \"boolean\";\n }\n // is everything else a text? What about strinfied dates?\n // we might need column metadata access here if we do\n return \"text\";\n }\n\n /**\n * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays\n * for columns with array types (e.g. text[], int4[]). These create\n * multidimensional arrays that can be \"ragged\" (sub-arrays with different\n * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional\n * arrays from JSON, so we need to drop these values.\n */\n private static safeStavalues(values: unknown[] | null): unknown[] | null {\n if (!values || values.length === 0) return values;\n if (values.some((v) => Array.isArray(v))) {\n console.warn(\"Discarding ragged multidimensional stavalues array\");\n return null;\n }\n return values;\n }\n\n /**\n * When inserting fake stats for existing tables and indexes, we also need to\n * account for data on newly created indexes by the optimizer.\n *\n * However we assume that index reltuples = table reltuples.\n * Meaning this logic is going to be a little off for partial indexes\n * or posting lists for duplicate values in btrees.\n * Meaning the deduplication that happens in pg side\n * 4 reltuples [(2, 1),(2, 9),(2, 4),(2, 8)] -> 1 reltuple [2, (1, 9, 4, 8)]\n * does not get accounted for here.\n */\n private async getAdditionalIndexReltuples(\n tx: PostgresTransaction,\n ): Promise<ComputedReltuples[]> {\n if (this.additionalIndexes.length === 0) {\n return [];\n }\n\n type ColumnAttlenRow = {\n index_name: string;\n schema_name: string;\n table_name: string;\n column_name: string;\n attlen: number | null;\n fillfactor: number;\n };\n\n const indexNames = this.additionalIndexes.map((idx) => idx.name.toString());\n const rows = await tx\n .exec<ColumnAttlenRow>(\n dedent`\n SELECT\n i.relname AS index_name,\n n.nspname AS schema_name,\n t.relname AS table_name,\n a.attname AS column_name,\n CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END AS attlen,\n COALESCE(\n (SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1),\n 90\n ) AS fillfactor\n FROM pg_class i\n JOIN pg_index ix ON ix.indexrelid = i.oid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN unnest(ix.indkey) AS k(attnum) ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE i.relname IN (SELECT jsonb_array_elements_text($1::jsonb))\n AND k.attnum > 0\n `,\n [indexNames],\n )\n .catch((err) => {\n console.error(\n \"Something went wrong querying additional index column metadata\",\n );\n console.error(err);\n return [];\n });\n\n const attlenByKey = new Map<string, number | null>();\n const fillfactorByIndex = new Map<string, number>();\n for (const row of rows) {\n attlenByKey.set(\n `${row.schema_name}.${row.table_name}.${row.column_name}`,\n row.attlen,\n );\n fillfactorByIndex.set(row.index_name, row.fillfactor);\n }\n\n return this.additionalIndexes.flatMap((idx) => {\n const tableStats = this.computedStats.reltuples.find((r) => {\n // making sure to follow normalization rules\n const sourceSchema = PgIdentifier.fromString(r.schema_name).toString();\n const sourceTable = PgIdentifier.fromString(r.relname).toString();\n\n const targetSchema = PgIdentifier.fromString(idx.schema).toString();\n const targetTable = PgIdentifier.fromString(idx.table).toString();\n return sourceSchema === targetSchema && sourceTable === targetTable;\n });\n\n if (!tableStats) {\n return [];\n }\n const columns = idx.columns.map((col) => ({\n attlen:\n attlenByKey.get(`${idx.schema}.${idx.table}.${col.column}`) ?? null,\n }));\n const fillfactor = fillfactorByIndex.get(idx.name.toString()) ?? 90;\n const amname = idx.indexMethod ?? \"btree\";\n const relpages = estimateIndexRelpages(\n tableStats.reltuples,\n columns,\n fillfactor / 100,\n amname,\n tableStats.relpages,\n );\n\n return [\n {\n relname: idx.name.toString(),\n schema_name: idx.schema,\n reltuples: tableStats.reltuples,\n relpages,\n relallvisible: relpages,\n relallfrozen: 0,\n },\n ];\n });\n }\n\n private async restoreStats17(tx: PostgresTransaction) {\n const warnings = {\n tablesNotInExports: [] as string[],\n tablesNotInTest: [] as string[],\n tableNotAnalyzed: [] as string[],\n statsMissing: [] as {\n statistic: string;\n table: string;\n schema: string;\n column: string;\n }[],\n };\n\n const processedTables = new Set<string>(\n this.ownMetadata.map((t) => `${t.schemaName}.${t.tableName}`),\n );\n\n const columnStatsUpdatePromise = tx\n .exec(Statistics.columnStatsSQL, [this.computedStats.columnStats])\n .catch((err) => {\n console.error(\"Something wrong wrong updating column stats\");\n console.error(err);\n throw err;\n });\n\n /**\n * Postgres has 5 different slots for storing statistics per column and a potentially unlimited\n * number of statistic types to choose from. Each code in `stakindN` can mean different things.\n * Some statistics are just numerical values such as `n_distinct` and `correlation`, meaning\n * they're only derived from `stanumbersN` and the value of `stanumbersN` is never read.\n * Others take advantage of the `stavaluesN` columns which use `anyarray` type to store\n * concrete values internally for things like histogram bounds.\n * Unfortunately we cannot change anyarrays without a C extension.\n *\n * (1) = most common values\n * (2) = scalar histogram\n * (3) = correlation <- can change\n * (4) = most common elements\n * (5) = distinct elem count histogram <- can change\n * (6) = length histogram (?) These don't appear in pg_stats\n * (7) = bounds histogram (?) These don't appear in pg_stats\n * (N) = potentially many more kinds of statistics. But postgres <=18 only uses these 7.\n *\n * What we're doing here is setting ANY statistic we cannot directly control\n * (anything that relies on stavaluesN) to 0 to make sure the planner isn't influenced by what\n * what the db collected from the test data.\n * Because we do our tests with `generic_plan` it seems it's already unlikely that the planner will be\n * using things like common values or histogram bounds to make the planning decisions we care about.\n * This is a just in case.\n */\n const reltuplesQuery = dedent`\n update pg_class p\n set reltuples = v.reltuples,\n relpages = v.relpages,\n -- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,\n relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end\n from jsonb_to_recordset($1::jsonb)\n as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)\n where p.relname = v.relname\n and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)\n returning p.relname, p.relnamespace, p.reltuples, p.relpages;\n `;\n\n const additionalIndexReltuples = await this.getAdditionalIndexReltuples(tx);\n\n const reltuplesPromise = tx\n .exec(reltuplesQuery, [\n [...this.computedStats.reltuples, ...additionalIndexReltuples],\n ])\n .catch((err) => {\n console.error(\"Something went wrong updating reltuples/relpages\");\n console.error(err);\n return err;\n });\n\n if (this.exportedMetadata) {\n for (const table of this.exportedMetadata) {\n const tableExists = processedTables.has(\n `${table.schemaName}.${table.tableName}`,\n );\n if (tableExists && table.reltuples === -1) {\n console.warn(\n `Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`,\n );\n warnings.tableNotAnalyzed.push(\n `${table.schemaName}.${table.tableName}`,\n );\n }\n if (tableExists) {\n continue;\n }\n warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);\n }\n }\n const [statsUpdates, reltuplesUpdates] = await Promise.all([\n columnStatsUpdatePromise,\n reltuplesPromise,\n ]);\n const updatedColumnsProperly = statsUpdates\n ? statsUpdates.length === this.computedStats.columnStats.length\n : true;\n if (!updatedColumnsProperly) {\n console.error(`Did not update expected column stats`);\n }\n if (reltuplesUpdates.length !== this.computedStats.reltuples.length) {\n console.error(`Did not update expected reltuples/relpages`);\n }\n return warnings;\n }\n\n private static readonly columnStatsSQL = dedent`\n WITH input AS (\n SELECT\n c.oid AS starelid,\n a.attnum AS staattnum,\n v.stainherit,\n v.stanullfrac,\n v.stawidth,\n v.stadistinct,\n v.stakind1,\n v.stakind2,\n v.stakind3,\n v.stakind4,\n v.stakind5,\n v.staop1,\n v.staop2,\n v.staop3,\n v.staop4,\n v.staop5,\n v.stacoll1,\n v.stacoll2,\n v.stacoll3,\n v.stacoll4,\n v.stacoll5,\n v.stanumbers1,\n v.stanumbers2,\n v.stanumbers3,\n v.stanumbers4,\n v.stanumbers5,\n v.stavalues1,\n v.stavalues2,\n v.stavalues3,\n v.stavalues4,\n v.stavalues5,\n _value_type1,\n _value_type2,\n _value_type3,\n _value_type4,\n _value_type5\n FROM jsonb_to_recordset($1::jsonb) AS v(\n schema_name text,\n table_name text,\n column_name text,\n stainherit boolean,\n stanullfrac real,\n stawidth integer,\n stadistinct real,\n stakind1 real,\n stakind2 real,\n stakind3 real,\n stakind4 real,\n stakind5 real,\n staop1 oid,\n staop2 oid,\n staop3 oid,\n staop4 oid,\n staop5 oid,\n stacoll1 oid,\n stacoll2 oid,\n stacoll3 oid,\n stacoll4 oid,\n stacoll5 oid,\n stanumbers1 real[],\n stanumbers2 real[],\n stanumbers3 real[],\n stanumbers4 real[],\n stanumbers5 real[],\n stavalues1 text[],\n stavalues2 text[],\n stavalues3 text[],\n stavalues4 text[],\n stavalues5 text[],\n _value_type1 text,\n _value_type2 text,\n _value_type3 text,\n _value_type4 text,\n _value_type5 text\n )\n JOIN pg_class c ON c.relname = v.table_name\n JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name\n ),\n updated AS (\n UPDATE pg_statistic s\n SET\n stanullfrac = i.stanullfrac,\n stawidth = i.stawidth,\n stadistinct = i.stadistinct,\n stakind1 = i.stakind1,\n stakind2 = i.stakind2,\n stakind3 = i.stakind3,\n stakind4 = i.stakind4,\n stakind5 = i.stakind5,\n staop1 = i.staop1,\n staop2 = i.staop2,\n staop3 = i.staop3,\n staop4 = i.staop4,\n staop5 = i.staop5,\n stacoll1 = i.stacoll1,\n stacoll2 = i.stacoll2,\n stacoll3 = i.stacoll3,\n stacoll4 = i.stacoll4,\n stacoll5 = i.stacoll5,\n stanumbers1 = i.stanumbers1,\n stanumbers2 = i.stanumbers2,\n stanumbers3 = i.stanumbers3,\n stanumbers4 = i.stanumbers4,\n stanumbers5 = i.stanumbers5,\n stavalues1 = case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n stavalues2 = case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n stavalues3 = case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n stavalues4 = case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n stavalues5 = case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- stavalues1 = i.stavalues1,\n -- stavalues2 = i.stavalues2,\n -- stavalues3 = i.stavalues3,\n -- stavalues4 = i.stavalues4,\n -- stavalues5 = i.stavalues5\n FROM input i\n WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit\n RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5\n ),\n inserted as (\n INSERT INTO pg_statistic (\n starelid, staattnum, stainherit,\n stanullfrac, stawidth, stadistinct,\n stakind1, stakind2, stakind3, stakind4, stakind5,\n staop1, staop2, staop3, staop4, staop5,\n stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,\n stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,\n stavalues1, stavalues2, stavalues3, stavalues4, stavalues5\n )\n SELECT\n i.starelid, i.staattnum, i.stainherit,\n i.stanullfrac, i.stawidth, i.stadistinct,\n i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,\n i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,\n i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,\n i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,\n -- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,\n case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5\n FROM input i\n LEFT JOIN updated u\n ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit\n WHERE u.starelid IS NULL\n returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5\n )\n select * from updated union all (select * from inserted); -- @qd_introspection`;\n\n static async dumpStats(\n db: PostgresTransaction,\n postgresVersion: PostgresVersion,\n ): Promise<ExportedStats[]> {\n console.log(`dumping stats for postgres ${gray(postgresVersion)}`);\n const stats = await db.exec<{ json_agg: ExportedStats[] }>(DUMP_STATS_SQL);\n const out = z.array(ExportedStats).parse(stats[0].json_agg);\n return out;\n }\n\n /**\n * Returns all indexes in the database.\n * ONLY handles regular btree indexes\n */\n async getExistingIndexes(): Promise<IndexedTable[]> {\n const indexes = await this.db.exec<IndexedTable>(`\n WITH partitioned_tables AS (\n SELECT\n inhparent::regclass AS parent_table,\n inhrelid::regclass AS partition_table\n FROM\n pg_inherits\n )\n SELECT\n n.nspname AS schema_name,\n COALESCE(pt.parent_table::text, t.relname) AS table_name,\n i.relname AS index_name,\n ix.indisprimary as is_primary,\n ix.indisunique as is_unique,\n am.amname AS index_type,\n array_agg(\n CASE\n -- Handle regular columns\n WHEN a.attname IS NOT NULL THEN\n json_build_object('name', a.attname, 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n -- Handle expressions\n ELSE\n json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n END\n ORDER BY array_position(ix.indkey, k.attnum)\n ) AS index_columns\n FROM\n pg_class t\n LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table\n JOIN pg_index ix ON t.oid = ix.indrelid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON i.relam = am.oid\n LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true\n LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid\n LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]\n JOIN pg_namespace n ON t.relnamespace = n.oid\n WHERE\n n.nspname not like 'pg_%' and\n n.nspname <> 'information_schema'\n GROUP BY\n n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique\n ORDER BY\n COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection\n `);\n return indexes;\n }\n}\n\nexport type ColumnMetadata = {\n columnName: string;\n dataType: string;\n isNullable: boolean;\n stats: ColumnStats | null;\n};\n\ntype ColumnStats = {\n stainherit: boolean;\n stanullfrac: number;\n stawidth: number;\n stadistinct: number;\n stakind1: number;\n stakind2: number;\n stakind3: number;\n stakind4: number;\n stakind5: number;\n staop1: number;\n staop2: number;\n staop3: number;\n staop4: number;\n staop5: number;\n stacoll1: number;\n stacoll2: number;\n stacoll3: number;\n stacoll4: number;\n stacoll5: number;\n stanumbers1: number;\n stanumbers2: number;\n stanumbers3: number;\n stanumbers4: number;\n stanumbers5: number;\n};\n\nexport type TableMetadata = {\n tableName: string;\n schemaName: string;\n reltuples: number;\n relpages: number;\n relallvisible: number;\n relallfrozen?: number;\n columns: ColumnMetadata[];\n};\n\ntype TableName = string;\nexport type TableStats = {\n tupleEstimate: bigint;\n pageCount: number;\n};\n\nexport type SerializeResult = {\n schema: TableMetadata[];\n serialized: string;\n sampledRecords: Record<TableName, number>;\n};\n\nexport type IndexOrder = \"ASC\" | \"DESC\";\n\nexport type IndexedTable = {\n index_columns: Array<{ name: string; order: IndexOrder; opclass?: string }>;\n is_primary: boolean;\n is_unique: boolean;\n index_name: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n index_type: \"btree\" | \"gin\" | (string & {});\n // this is always public\n schema_name: string;\n table_name: string;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statistics.mjs","names":[],"sources":["../../src/optimizer/statistics.ts"],"sourcesContent":["import { gray } from \"colorette\";\nimport dedent from \"dedent\";\nimport { z } from \"zod\";\nimport type {\n Postgres,\n PostgresTransaction,\n PostgresVersion,\n} from \"../sql/database.ts\";\nimport { PgIdentifier } from \"../sql/pg-identifier.js\";\nimport type { IndexToCreate } from \"./genalgo.js\";\n\ntype StaValueKind = \"real\" | \"text\" | \"boolean\" | null;\n\nexport type Path = string;\n\nexport const StatisticsSource = z.union([\n z.object({\n kind: z.literal(\"path\"),\n path: z.string().min(1),\n }),\n z.object({\n kind: z.literal(\"inline\"),\n }),\n]);\n\nexport const ExportedStatsStatistics = z.object({\n stawidth: z.number(),\n stainherit: z.boolean().default(false),\n // 0 representing unknown\n stadistinct: z.number(),\n // this has no \"nullable\" state\n stanullfrac: z.number(),\n stakind1: z.number().min(0),\n stakind2: z.number().min(0),\n stakind3: z.number().min(0),\n stakind4: z.number().min(0),\n stakind5: z.number().min(0),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n // theoretically... this could only be strings and numbers\n // but we don't have a crystal ball\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n});\n\nexport const ExportedStatsColumns = z.object({\n columnName: z.string(),\n attlen: z.number().nullable(),\n dataType: z.string().optional(),\n stats: ExportedStatsStatistics.nullable(),\n});\n\nexport const ExportedStatsIndex = z.object({\n indexName: z.string(),\n amname: z.string().default(\"btree\"),\n relpages: z.number(),\n reltuples: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n fillfactor: z.number().default(90),\n columns: z.array(z.object({ attlen: z.number().nullable() })).default([]),\n});\n\n// This should match the output of the `_qd_dump_stats` function in the analyzer README.md\n// Need to make sure this is versioned to accept ALL potential outputs from every version of\n// dump functions we make public\nexport const ExportedStatsV1 = z.object({\n tableName: z.string(),\n schemaName: z.string(),\n // can be negative\n relpages: z.number(),\n // can be negative\n reltuples: z.number(),\n relallvisible: z.number(),\n // only postgres 18+\n relallfrozen: z.number().optional(),\n columns: z.array(ExportedStatsColumns).default([]),\n indexes: z.array(ExportedStatsIndex),\n});\n\nexport const ExportedStats = z.union([ExportedStatsV1]);\n\nexport type ExportedStats = z.infer<typeof ExportedStats>;\n\nexport const StatisticsMode = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"fromAssumption\"),\n reltuples: z.number().min(0),\n }),\n z.object({\n kind: z.literal(\"fromStatisticsExport\"),\n stats: z.array(ExportedStats),\n source: StatisticsSource,\n }),\n]);\n\nexport type StatisticsMode = z.infer<typeof StatisticsMode>;\n\nexport const ComputedColumnStats = z.object({\n schema_name: z.string(),\n table_name: z.string(),\n column_name: z.string(),\n data_type: z.string().optional(),\n stainherit: z.boolean(),\n stanullfrac: z.number(),\n stawidth: z.number(),\n stadistinct: z.number(),\n stakind1: z.number(),\n stakind2: z.number(),\n stakind3: z.number(),\n stakind4: z.number(),\n stakind5: z.number(),\n staop1: z.string(),\n staop2: z.string(),\n staop3: z.string(),\n staop4: z.string(),\n staop5: z.string(),\n stacoll1: z.string(),\n stacoll2: z.string(),\n stacoll3: z.string(),\n stacoll4: z.string(),\n stacoll5: z.string(),\n stanumbers1: z.array(z.number()).nullable(),\n stanumbers2: z.array(z.number()).nullable(),\n stanumbers3: z.array(z.number()).nullable(),\n stanumbers4: z.array(z.number()).nullable(),\n stanumbers5: z.array(z.number()).nullable(),\n stavalues1: z.array(z.any()).nullable(),\n stavalues2: z.array(z.any()).nullable(),\n stavalues3: z.array(z.any()).nullable(),\n stavalues4: z.array(z.any()).nullable(),\n stavalues5: z.array(z.any()).nullable(),\n _value_type1: z.string().nullable(),\n _value_type2: z.string().nullable(),\n _value_type3: z.string().nullable(),\n _value_type4: z.string().nullable(),\n _value_type5: z.string().nullable(),\n});\n\nexport type ComputedColumnStats = z.infer<typeof ComputedColumnStats>;\n\nexport const ComputedReltuples = z.object({\n relname: z.string(),\n schema_name: z.string(),\n reltuples: z.number(),\n relpages: z.number(),\n relallvisible: z.number(),\n relallfrozen: z.number().optional(),\n});\n\nexport type ComputedReltuples = z.infer<typeof ComputedReltuples>;\n\nexport const ComputedStats = z.object({\n columnStats: z.array(ComputedColumnStats),\n reltuples: z.array(ComputedReltuples),\n});\n\nexport type ComputedStats = z.infer<typeof ComputedStats>;\n\nconst DEFAULT_RELTUPLES = 10_000_000;\nconst DEFAULT_RELPAGES = 1;\n// it's _very_ rare that the default page size is ever changed\nconst DEFAULT_PAGE_SIZE = 2 ** 13;\n\nfunction estimateStawidth(col: { attlen?: number | null }): number {\n return col.attlen ?? 32;\n}\n\nfunction estimateRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n): number {\n const rowWidth =\n // 23 byte tuple header + 4 bytes alignment/null bitmap\n columns.reduce((sum, col) => sum + estimateStawidth(col), 0) + 27;\n return Math.ceil((reltuples * rowWidth) / DEFAULT_PAGE_SIZE);\n}\n\nfunction estimateIndexRelpages(\n reltuples: number,\n columns: { attlen: number | null }[],\n fillfactor: number,\n amname: string,\n tableRelpages: number,\n): number {\n if (amname === \"gin\") {\n // GIN has an inverted structure; distinct element counts per row are unknown\n // without real data, so fall back to a ratio of the table page count\n return Math.ceil(tableRelpages * 0.3);\n }\n // 16 bytes btree entry overhead per key in addition to the key width\n const keyWidth = columns.reduce(\n (sum, col) => sum + estimateStawidth(col) + 16,\n 0,\n );\n return Math.ceil((reltuples * keyWidth) / DEFAULT_PAGE_SIZE / fillfactor);\n}\n\nexport const DUMP_STATS_SQL = dedent`\n WITH table_columns AS (\n SELECT\n cl.relname,\n n.nspname,\n cl.reltuples,\n cl.relpages,\n cl.relallvisible,\n -- cl.relallfrozen,\n json_agg(\n json_build_object(\n 'columnName', a.attname,\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END,\n 'dataType', t.typname,\n 'stats', (\n SELECT json_build_object(\n 'starelid', s.starelid,\n 'staattnum', s.staattnum,\n 'stanullfrac', s.stanullfrac,\n 'stawidth', s.stawidth,\n 'stadistinct', s.stadistinct,\n 'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,\n 'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,\n 'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,\n 'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,\n 'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,\n 'stavalues1', s.stavalues1,\n 'stavalues2', s.stavalues2,\n 'stavalues3', s.stavalues3,\n 'stavalues4', s.stavalues4,\n 'stavalues5', s.stavalues5\n )\n FROM pg_statistic s\n WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum\n )\n )\n ORDER BY a.attnum\n ) AS columns\n FROM pg_class cl\n JOIN pg_namespace n ON n.oid = cl.relnamespace\n JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attnum > 0 AND NOT a.attisdropped\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE cl.relkind = 'r'\n AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'tiger', 'tiger_data', 'topology')\n AND cl.relname NOT IN ('pg_stat_statements', 'pg_stat_statements_info')\n GROUP BY cl.relname, n.nspname, cl.reltuples, cl.relpages, cl.relallvisible\n ),\n table_indexes AS (\n SELECT\n t.relname AS table_name,\n json_agg(\n json_build_object(\n 'indexName', i.relname,\n 'amname', am.amname,\n 'reltuples', i.reltuples,\n 'relpages', i.relpages,\n 'relallvisible', i.relallvisible,\n -- 'relallfrozen', i.relallfrozen,\n 'fillfactor', COALESCE(\n (\n SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1\n ),\n 90\n ),\n 'columns', COALESCE(\n (\n SELECT json_agg(json_build_object(\n 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END\n ) ORDER BY col_pos.ord)\n FROM unnest(ix.indkey) WITH ORDINALITY AS col_pos(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = col_pos.attnum\n WHERE col_pos.attnum > 0\n ),\n '[]'::json\n )\n )\n ) AS indexes\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON am.oid = i.relam\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE t.relname NOT LIKE 'pg_%'\n AND n.nspname <> 'information_schema'\n AND n.nspname NOT IN ('tiger', 'tiger_data', 'topology')\n GROUP BY t.relname\n )\n SELECT json_agg(\n json_build_object(\n 'tableName', tc.relname,\n 'schemaName', tc.nspname,\n 'reltuples', tc.reltuples,\n 'relpages', tc.relpages,\n 'relallvisible', tc.relallvisible,\n -- 'relallfrozen', tc.relallfrozen,\n 'columns', COALESCE(tc.columns, '[]'::json),\n 'indexes', COALESCE(ti.indexes, '[]'::json)\n )\n )\n FROM table_columns tc\n LEFT JOIN table_indexes ti\n ON ti.table_name = tc.relname\n`;\n\nexport class Statistics {\n readonly mode: StatisticsMode;\n readonly computedStats: ComputedStats;\n private readonly exportedMetadata: ExportedStats[] | undefined;\n private additionalIndexes: IndexToCreate[] = [];\n // preventing accidental internal mutations\n static readonly defaultStatsMode: StatisticsMode = Object.freeze({\n kind: \"fromAssumption\",\n reltuples: DEFAULT_RELTUPLES,\n });\n constructor(\n private readonly db: Postgres,\n public readonly postgresVersion: PostgresVersion,\n public readonly ownMetadata: ExportedStats[],\n statsMode: StatisticsMode,\n ) {\n if (statsMode) {\n this.mode = statsMode;\n if (statsMode.kind === \"fromStatisticsExport\") {\n this.exportedMetadata = statsMode.stats;\n }\n } else {\n this.mode = Statistics.defaultStatsMode;\n }\n this.computedStats = this.buildComputedStats();\n }\n\n setAdditionalIndexes(additionalIndexes: IndexToCreate[]): void {\n this.additionalIndexes = additionalIndexes;\n }\n\n private buildComputedStats(): ComputedStats {\n const columnStats: ComputedColumnStats[] = [];\n const reltuples: ComputedReltuples[] = [];\n\n for (const table of this.ownMetadata) {\n const targetTable = this.exportedMetadata?.find(\n (m) =>\n m.tableName === table.tableName && m.schemaName === table.schemaName,\n );\n const target = targetTable?.columns ?? table.columns;\n\n for (const column of target) {\n const { stats } = column;\n if (!stats || this.mode.kind === \"fromAssumption\") {\n const stawidth = stats?.stawidth || estimateStawidth(column);\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: false,\n stanullfrac: 0.04,\n stawidth,\n stadistinct: -0.9,\n stakind1: 0,\n stakind2: 0,\n stakind3: 3,\n stakind4: 0,\n stakind5: 0,\n stacoll1: \"0\",\n stacoll2: \"0\",\n stacoll3: \"0\",\n stacoll4: \"0\",\n stacoll5: \"0\",\n staop1: \"0\",\n staop2: \"0\",\n staop3: \"0\",\n staop4: \"0\",\n staop5: \"0\",\n stanumbers1: null,\n stanumbers2: null,\n stanumbers3: [0.9],\n stanumbers4: null,\n stanumbers5: null,\n stavalues1: null,\n stavalues2: null,\n stavalues3: null,\n stavalues4: null,\n stavalues5: null,\n _value_type1: \"real\",\n _value_type2: \"real\",\n _value_type3: \"real\",\n _value_type4: \"real\",\n _value_type5: \"real\",\n });\n } else {\n columnStats.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n data_type: column.dataType,\n stainherit: stats.stainherit ?? false,\n stanullfrac: stats.stanullfrac,\n stawidth: stats.stawidth,\n stadistinct: stats.stadistinct,\n stakind1: stats.stakind1,\n stakind2: stats.stakind2,\n stakind3: stats.stakind3,\n stakind4: stats.stakind4,\n stakind5: stats.stakind5,\n staop1: stats.staop1,\n staop2: stats.staop2,\n staop3: stats.staop3,\n staop4: stats.staop4,\n staop5: stats.staop5,\n stacoll1: stats.stacoll1,\n stacoll2: stats.stacoll2,\n stacoll3: stats.stacoll3,\n stacoll4: stats.stacoll4,\n stacoll5: stats.stacoll5,\n stanumbers1: stats.stanumbers1,\n stanumbers2: stats.stanumbers2,\n stanumbers3: stats.stanumbers3,\n stanumbers4: stats.stanumbers4,\n stanumbers5: stats.stanumbers5,\n stavalues1: Statistics.safeStavalues(stats.stavalues1),\n stavalues2: Statistics.safeStavalues(stats.stavalues2),\n stavalues3: Statistics.safeStavalues(stats.stavalues3),\n stavalues4: Statistics.safeStavalues(stats.stavalues4),\n stavalues5: Statistics.safeStavalues(stats.stavalues5),\n _value_type1: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues1),\n ),\n _value_type2: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues2),\n ),\n _value_type3: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues3),\n ),\n _value_type4: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues4),\n ),\n _value_type5: this.stavalueKind(\n Statistics.safeStavalues(stats.stavalues5),\n ),\n });\n }\n }\n\n let tableReltuples: number;\n let tableRelpages: number;\n let relallvisible = 0;\n let relallfrozen: number | undefined;\n\n if (this.mode.kind === \"fromAssumption\") {\n tableReltuples = this.mode.reltuples;\n tableRelpages = estimateRelpages(tableReltuples, table.columns);\n } else if (targetTable) {\n tableReltuples = targetTable.reltuples;\n tableRelpages = targetTable.relpages;\n relallvisible = targetTable.relallvisible;\n relallfrozen = targetTable.relallfrozen;\n } else {\n tableReltuples = DEFAULT_RELTUPLES;\n tableRelpages = DEFAULT_RELPAGES;\n }\n\n reltuples.push({\n relname: table.tableName,\n schema_name: table.schemaName,\n reltuples: tableReltuples,\n relpages: tableRelpages,\n relallfrozen,\n relallvisible,\n });\n\n if (this.mode.kind === \"fromAssumption\") {\n for (const index of table.indexes) {\n const indexRelpages = estimateIndexRelpages(\n this.mode.reltuples,\n index.columns,\n index.fillfactor / 100,\n index.amname,\n tableRelpages,\n );\n reltuples.push({\n relname: index.indexName,\n schema_name: table.schemaName,\n reltuples: this.mode.reltuples,\n relpages: indexRelpages,\n relallfrozen: 0,\n relallvisible: indexRelpages,\n });\n }\n } else if (targetTable) {\n for (const index of targetTable.indexes) {\n reltuples.push({\n relname: index.indexName,\n schema_name: targetTable.schemaName,\n reltuples: index.reltuples,\n relpages: index.relpages,\n relallfrozen: index.relallfrozen,\n relallvisible: index.relallvisible,\n });\n }\n }\n }\n\n return { columnStats, reltuples };\n }\n\n static statsModeFromAssumption({\n reltuples,\n }: {\n reltuples: number;\n }): StatisticsMode {\n return {\n kind: \"fromAssumption\",\n reltuples,\n };\n }\n\n /**\n * Create a statistic mode from stats exported from another database\n **/\n static statsModeFromExport(stats: ExportedStats[]): StatisticsMode {\n return {\n kind: \"fromStatisticsExport\",\n source: { kind: \"inline\" },\n stats,\n };\n }\n\n static async fromPostgres(\n db: Postgres,\n statsMode: StatisticsMode,\n ): Promise<Statistics> {\n const version = await db.serverNum();\n const ownStats = await Statistics.dumpStats(db, version);\n return new Statistics(db, version, ownStats, statsMode);\n }\n\n restoreStats(tx: PostgresTransaction) {\n // if (this.postgresVersion < \"180000\") {\n return this.restoreStats17(tx);\n // }\n // return this.restoreStats18(tx);\n }\n\n approximateTotalRows() {\n if (!this.exportedMetadata) {\n return 0;\n }\n let totalRows = 0;\n for (const table of this.exportedMetadata) {\n totalRows += table.reltuples;\n }\n return totalRows;\n }\n\n /**\n * We have to cast stavaluesN to the correct type\n * This derives that type for us so it can be used in `array_in`\n */\n private stavalueKind(values: unknown[] | null): StaValueKind {\n if (!values || values.length === 0) {\n return null;\n }\n const [elem] = values;\n if (typeof elem === \"number\") {\n return \"real\";\n } else if (typeof elem === \"boolean\") {\n return \"boolean\";\n }\n // is everything else a text? What about strinfied dates?\n // we might need column metadata access here if we do\n return \"text\";\n }\n\n /**\n * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays\n * for columns with array types (e.g. text[], int4[]). These create\n * multidimensional arrays that can be \"ragged\" (sub-arrays with different\n * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional\n * arrays from JSON, so we need to drop these values.\n */\n private static safeStavalues(values: unknown[] | null): unknown[] | null {\n if (!values || values.length === 0) return values;\n if (values.some((v) => Array.isArray(v))) {\n console.warn(\"Discarding ragged multidimensional stavalues array\");\n return null;\n }\n return values;\n }\n\n /**\n * When inserting fake stats for existing tables and indexes, we also need to\n * account for data on newly created indexes by the optimizer.\n *\n * However we assume that index reltuples = table reltuples.\n * Meaning this logic is going to be a little off for partial indexes\n * or posting lists for duplicate values in btrees.\n * Meaning the deduplication that happens in pg side\n * 4 reltuples [(2, 1),(2, 9),(2, 4),(2, 8)] -> 1 reltuple [2, (1, 9, 4, 8)]\n * does not get accounted for here.\n */\n private async getAdditionalIndexReltuples(\n tx: PostgresTransaction,\n ): Promise<ComputedReltuples[]> {\n if (this.additionalIndexes.length === 0) {\n return [];\n }\n\n type ColumnAttlenRow = {\n index_name: string;\n schema_name: string;\n table_name: string;\n column_name: string;\n attlen: number | null;\n fillfactor: number;\n };\n\n const indexNames = this.additionalIndexes.map((idx) => idx.name.toString());\n const rows = await tx\n .exec<ColumnAttlenRow>(\n dedent`\n SELECT\n i.relname AS index_name,\n n.nspname AS schema_name,\n t.relname AS table_name,\n a.attname AS column_name,\n CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END AS attlen,\n COALESCE(\n (SELECT (regexp_match(opt, 'fillfactor=(\\\\d+)'))[1]::integer\n FROM unnest(i.reloptions) AS opt\n WHERE opt LIKE 'fillfactor=%'\n LIMIT 1),\n 90\n ) AS fillfactor\n FROM pg_class i\n JOIN pg_index ix ON ix.indexrelid = i.oid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN unnest(ix.indkey) AS k(attnum) ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE i.relname IN (SELECT jsonb_array_elements_text($1::jsonb))\n AND k.attnum > 0\n `,\n [indexNames],\n )\n .catch((err) => {\n console.error(\n \"Something went wrong querying additional index column metadata\",\n );\n console.error(err);\n return [];\n });\n\n const attlenByKey = new Map<string, number | null>();\n const fillfactorByIndex = new Map<string, number>();\n for (const row of rows) {\n attlenByKey.set(\n `${row.schema_name}.${row.table_name}.${row.column_name}`,\n row.attlen,\n );\n fillfactorByIndex.set(row.index_name, row.fillfactor);\n }\n\n return this.additionalIndexes.flatMap((idx) => {\n const tableStats = this.computedStats.reltuples.find((r) => {\n // making sure to follow normalization rules\n const sourceSchema = PgIdentifier.fromString(r.schema_name).toString();\n const sourceTable = PgIdentifier.fromString(r.relname).toString();\n\n const targetSchema = PgIdentifier.fromString(idx.schema).toString();\n const targetTable = PgIdentifier.fromString(idx.table).toString();\n return sourceSchema === targetSchema && sourceTable === targetTable;\n });\n\n if (!tableStats) {\n return [];\n }\n const columns = idx.columns.map((col) => ({\n attlen:\n attlenByKey.get(`${idx.schema}.${idx.table}.${col.column}`) ?? null,\n }));\n const fillfactor = fillfactorByIndex.get(idx.name.toString()) ?? 90;\n const amname = idx.indexMethod ?? \"btree\";\n const relpages = estimateIndexRelpages(\n tableStats.reltuples,\n columns,\n fillfactor / 100,\n amname,\n tableStats.relpages,\n );\n\n return [\n {\n relname: idx.name.toString(),\n schema_name: idx.schema,\n reltuples: tableStats.reltuples,\n relpages,\n relallvisible: relpages,\n relallfrozen: 0,\n },\n ];\n });\n }\n\n private async restoreStats17(tx: PostgresTransaction) {\n const warnings = {\n tablesNotInExports: [] as string[],\n tablesNotInTest: [] as string[],\n tableNotAnalyzed: [] as string[],\n statsMissing: [] as {\n statistic: string;\n table: string;\n schema: string;\n column: string;\n }[],\n };\n\n const processedTables = new Set<string>(\n this.ownMetadata.map((t) => `${t.schemaName}.${t.tableName}`),\n );\n\n const columnStatsUpdatePromise = tx\n .exec(Statistics.columnStatsSQL, [this.computedStats.columnStats])\n .catch((err) => {\n console.error(\"Something wrong wrong updating column stats\");\n console.error(err);\n throw err;\n });\n\n /**\n * Postgres has 5 different slots for storing statistics per column and a potentially unlimited\n * number of statistic types to choose from. Each code in `stakindN` can mean different things.\n * Some statistics are just numerical values such as `n_distinct` and `correlation`, meaning\n * they're only derived from `stanumbersN` and the value of `stanumbersN` is never read.\n * Others take advantage of the `stavaluesN` columns which use `anyarray` type to store\n * concrete values internally for things like histogram bounds.\n * Unfortunately we cannot change anyarrays without a C extension.\n *\n * (1) = most common values\n * (2) = scalar histogram\n * (3) = correlation <- can change\n * (4) = most common elements\n * (5) = distinct elem count histogram <- can change\n * (6) = length histogram (?) These don't appear in pg_stats\n * (7) = bounds histogram (?) These don't appear in pg_stats\n * (N) = potentially many more kinds of statistics. But postgres <=18 only uses these 7.\n *\n * What we're doing here is setting ANY statistic we cannot directly control\n * (anything that relies on stavaluesN) to 0 to make sure the planner isn't influenced by what\n * what the db collected from the test data.\n * Because we do our tests with `generic_plan` it seems it's already unlikely that the planner will be\n * using things like common values or histogram bounds to make the planning decisions we care about.\n * This is a just in case.\n */\n const reltuplesQuery = dedent`\n update pg_class p\n set reltuples = v.reltuples,\n relpages = v.relpages,\n -- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,\n relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end\n from jsonb_to_recordset($1::jsonb)\n as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)\n where p.relname = v.relname\n and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)\n returning p.relname, p.relnamespace, p.reltuples, p.relpages;\n `;\n\n const additionalIndexReltuples = await this.getAdditionalIndexReltuples(tx);\n\n const reltuplesPromise = tx\n .exec(reltuplesQuery, [\n [...this.computedStats.reltuples, ...additionalIndexReltuples],\n ])\n .catch((err) => {\n console.error(\"Something went wrong updating reltuples/relpages\");\n console.error(err);\n return err;\n });\n\n if (this.exportedMetadata) {\n for (const table of this.exportedMetadata) {\n const tableExists = processedTables.has(\n `${table.schemaName}.${table.tableName}`,\n );\n if (tableExists && table.reltuples === -1) {\n console.warn(\n `Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`,\n );\n warnings.tableNotAnalyzed.push(\n `${table.schemaName}.${table.tableName}`,\n );\n }\n if (tableExists) {\n continue;\n }\n warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);\n }\n }\n const [statsUpdates, reltuplesUpdates] = await Promise.all([\n columnStatsUpdatePromise,\n reltuplesPromise,\n ]);\n const updatedColumnsProperly = statsUpdates\n ? statsUpdates.length === this.computedStats.columnStats.length\n : true;\n if (!updatedColumnsProperly) {\n console.error(`Did not update expected column stats`);\n }\n if (reltuplesUpdates.length !== this.computedStats.reltuples.length) {\n console.error(`Did not update expected reltuples/relpages`);\n }\n return warnings;\n }\n\n private static readonly columnStatsSQL = dedent`\n WITH input AS (\n SELECT\n c.oid AS starelid,\n a.attnum AS staattnum,\n v.stainherit,\n v.stanullfrac,\n v.stawidth,\n v.stadistinct,\n v.stakind1,\n v.stakind2,\n v.stakind3,\n v.stakind4,\n v.stakind5,\n v.staop1,\n v.staop2,\n v.staop3,\n v.staop4,\n v.staop5,\n v.stacoll1,\n v.stacoll2,\n v.stacoll3,\n v.stacoll4,\n v.stacoll5,\n v.stanumbers1,\n v.stanumbers2,\n v.stanumbers3,\n v.stanumbers4,\n v.stanumbers5,\n v.stavalues1,\n v.stavalues2,\n v.stavalues3,\n v.stavalues4,\n v.stavalues5,\n _value_type1,\n _value_type2,\n _value_type3,\n _value_type4,\n _value_type5\n FROM jsonb_to_recordset($1::jsonb) AS v(\n schema_name text,\n table_name text,\n column_name text,\n stainherit boolean,\n stanullfrac real,\n stawidth integer,\n stadistinct real,\n stakind1 real,\n stakind2 real,\n stakind3 real,\n stakind4 real,\n stakind5 real,\n staop1 oid,\n staop2 oid,\n staop3 oid,\n staop4 oid,\n staop5 oid,\n stacoll1 oid,\n stacoll2 oid,\n stacoll3 oid,\n stacoll4 oid,\n stacoll5 oid,\n stanumbers1 real[],\n stanumbers2 real[],\n stanumbers3 real[],\n stanumbers4 real[],\n stanumbers5 real[],\n stavalues1 text[],\n stavalues2 text[],\n stavalues3 text[],\n stavalues4 text[],\n stavalues5 text[],\n _value_type1 text,\n _value_type2 text,\n _value_type3 text,\n _value_type4 text,\n _value_type5 text\n )\n JOIN pg_class c ON c.relname = v.table_name\n JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name\n ),\n updated AS (\n UPDATE pg_statistic s\n SET\n stanullfrac = i.stanullfrac,\n stawidth = i.stawidth,\n stadistinct = i.stadistinct,\n stakind1 = i.stakind1,\n stakind2 = i.stakind2,\n stakind3 = i.stakind3,\n stakind4 = i.stakind4,\n stakind5 = i.stakind5,\n staop1 = i.staop1,\n staop2 = i.staop2,\n staop3 = i.staop3,\n staop4 = i.staop4,\n staop5 = i.staop5,\n stacoll1 = i.stacoll1,\n stacoll2 = i.stacoll2,\n stacoll3 = i.stacoll3,\n stacoll4 = i.stacoll4,\n stacoll5 = i.stacoll5,\n stanumbers1 = i.stanumbers1,\n stanumbers2 = i.stanumbers2,\n stanumbers3 = i.stanumbers3,\n stanumbers4 = i.stanumbers4,\n stanumbers5 = i.stanumbers5,\n stavalues1 = case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n stavalues2 = case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n stavalues3 = case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n stavalues4 = case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n stavalues5 = case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- stavalues1 = i.stavalues1,\n -- stavalues2 = i.stavalues2,\n -- stavalues3 = i.stavalues3,\n -- stavalues4 = i.stavalues4,\n -- stavalues5 = i.stavalues5\n FROM input i\n WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit\n RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5\n ),\n inserted as (\n INSERT INTO pg_statistic (\n starelid, staattnum, stainherit,\n stanullfrac, stawidth, stadistinct,\n stakind1, stakind2, stakind3, stakind4, stakind5,\n staop1, staop2, staop3, staop4, staop5,\n stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,\n stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,\n stavalues1, stavalues2, stavalues3, stavalues4, stavalues5\n )\n SELECT\n i.starelid, i.staattnum, i.stainherit,\n i.stanullfrac, i.stawidth, i.stadistinct,\n i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,\n i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,\n i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,\n i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,\n -- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,\n case\n when i.stavalues1 is null then null\n else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)\n end,\n case\n when i.stavalues2 is null then null\n else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)\n end,\n case\n when i.stavalues3 is null then null\n else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)\n end,\n case\n when i.stavalues4 is null then null\n else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)\n end,\n case\n when i.stavalues5 is null then null\n else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)\n end\n -- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5\n FROM input i\n LEFT JOIN updated u\n ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit\n WHERE u.starelid IS NULL\n returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5\n )\n select * from updated union all (select * from inserted); -- @qd_introspection`;\n\n static async dumpStats(\n db: PostgresTransaction,\n postgresVersion: PostgresVersion,\n ): Promise<ExportedStats[]> {\n console.log(`dumping stats for postgres ${gray(postgresVersion)}`);\n const stats = await db.exec<{ json_agg: ExportedStats[] }>(DUMP_STATS_SQL);\n const out = z.array(ExportedStats).parse(stats[0].json_agg ?? []);\n return out;\n }\n\n /**\n * Returns all indexes in the database.\n * ONLY handles regular btree indexes\n */\n async getExistingIndexes(): Promise<IndexedTable[]> {\n const indexes = await this.db.exec<IndexedTable>(`\n WITH partitioned_tables AS (\n SELECT\n inhparent::regclass AS parent_table,\n inhrelid::regclass AS partition_table\n FROM\n pg_inherits\n )\n SELECT\n n.nspname AS schema_name,\n COALESCE(pt.parent_table::text, t.relname) AS table_name,\n i.relname AS index_name,\n ix.indisprimary as is_primary,\n ix.indisunique as is_unique,\n am.amname AS index_type,\n array_agg(\n CASE\n -- Handle regular columns\n WHEN a.attname IS NOT NULL THEN\n json_build_object('name', a.attname, 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n -- Handle expressions\n ELSE\n json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',\n CASE\n WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'\n ELSE 'ASC'\n END,\n 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)\n END\n ORDER BY array_position(ix.indkey, k.attnum)\n ) AS index_columns\n FROM\n pg_class t\n LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table\n JOIN pg_index ix ON t.oid = ix.indrelid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_am am ON i.relam = am.oid\n LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true\n LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid\n LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]\n JOIN pg_namespace n ON t.relnamespace = n.oid\n WHERE\n n.nspname not like 'pg_%' and\n n.nspname <> 'information_schema'\n GROUP BY\n n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique\n ORDER BY\n COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection\n `);\n return indexes;\n }\n}\n\nexport type ColumnMetadata = {\n columnName: string;\n dataType: string;\n isNullable: boolean;\n stats: ColumnStats | null;\n};\n\ntype ColumnStats = {\n stainherit: boolean;\n stanullfrac: number;\n stawidth: number;\n stadistinct: number;\n stakind1: number;\n stakind2: number;\n stakind3: number;\n stakind4: number;\n stakind5: number;\n staop1: number;\n staop2: number;\n staop3: number;\n staop4: number;\n staop5: number;\n stacoll1: number;\n stacoll2: number;\n stacoll3: number;\n stacoll4: number;\n stacoll5: number;\n stanumbers1: number;\n stanumbers2: number;\n stanumbers3: number;\n stanumbers4: number;\n stanumbers5: number;\n};\n\nexport type TableMetadata = {\n tableName: string;\n schemaName: string;\n reltuples: number;\n relpages: number;\n relallvisible: number;\n relallfrozen?: number;\n columns: ColumnMetadata[];\n};\n\ntype TableName = string;\nexport type TableStats = {\n tupleEstimate: bigint;\n pageCount: number;\n};\n\nexport type SerializeResult = {\n schema: TableMetadata[];\n serialized: string;\n sampledRecords: Record<TableName, number>;\n};\n\nexport type IndexOrder = \"ASC\" | \"DESC\";\n\nexport type IndexedTable = {\n index_columns: Array<{ name: string; order: IndexOrder; opclass?: string }>;\n is_primary: boolean;\n is_unique: boolean;\n index_name: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n index_type: \"btree\" | \"gin\" | (string & {});\n // this is always public\n schema_name: string;\n table_name: string;\n};\n"],"mappings":"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"}
|
package/dist/sql/nudges.cjs
CHANGED
|
@@ -84,6 +84,11 @@ function parseNudges(node, stack) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
if (require_ast_utils.is(node, "SelectStmt") && node.SelectStmt.limitCount && !isNullConstant(node.SelectStmt.limitCount) && !node.SelectStmt.sortClause) nudges.push({
|
|
88
|
+
kind: "LIMIT_WITHOUT_ORDER_BY",
|
|
89
|
+
severity: "WARNING",
|
|
90
|
+
message: "LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output"
|
|
91
|
+
});
|
|
87
92
|
if (require_ast_utils.is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
|
|
88
93
|
if (!require_ast_utils.is(sortItem, "SortBy")) continue;
|
|
89
94
|
const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
|
package/dist/sql/nudges.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudges.cjs","names":["is","isANode"],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"Prefer COUNT(*) over COUNT(column) or COUNT(1) — COUNT(*) counts rows without checking for NULLs. If you need to count non-NULL values, COUNT(column) is correct.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nudges.cjs","names":["is","isANode"],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\"\n | \"LIMIT_WITHOUT_ORDER_BY\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect LIMIT without ORDER BY (non-deterministic results)\n // LIMIT ALL parses as limitCount with A_Const.isnull — exclude it\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.limitCount &&\n !isNullConstant(node.SelectStmt.limitCount) &&\n !node.SelectStmt.sortClause\n ) {\n nudges.push({\n kind: \"LIMIT_WITHOUT_ORDER_BY\",\n severity: \"WARNING\",\n message:\n \"LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output\",\n });\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"Prefer COUNT(*) over COUNT(column) or COUNT(1) — COUNT(*) counts rows without checking for NULLs. If you need to count non-NULL values, COUNT(column) is correct.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
package/dist/sql/nudges.d.cts
CHANGED
|
@@ -4,7 +4,7 @@ import { KeysOfUnion } from "./ast-utils.cjs";
|
|
|
4
4
|
import { Node } from "@pgsql/types";
|
|
5
5
|
|
|
6
6
|
//#region src/sql/nudges.d.ts
|
|
7
|
-
type NudgeKind = "LARGE_IMPROVEMENT_FOUND" | "SMALL_IMPROVEMENT_FOUND" | "AVOID_SELECT_STAR" | "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE" | "MISSING_WHERE_CLAUSE" | "MISSING_LIMIT_CLAUSE" | "USE_IS_NULL_NOT_EQUALS" | "AVOID_DISTINCT_WITHOUT_REASON" | "MISSING_JOIN_CONDITION" | "AVOID_LEADING_WILDCARD_LIKE" | "CONSIDER_IN_INSTEAD_OF_MANY_ORS" | "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY" | "PREFER_NOT_EXISTS_OVER_NOT_IN" | "AVOID_ORDER_BY_RANDOM" | "PREFER_FILTER_OVER_CASE_IN_AGGREGATE" | "PREFER_UNION_ALL_OVER_UNION" | "NULLS_FIRST_IN_DESC_ORDER" | "AVOID_SCALAR_SUBQUERY_IN_SELECT" | "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK" | "PREFER_COUNT_STAR_OVER_COUNT_COLUMN" | "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES" | "CONSIDER_JSONB_CONTAINMENT_OPERATOR";
|
|
7
|
+
type NudgeKind = "LARGE_IMPROVEMENT_FOUND" | "SMALL_IMPROVEMENT_FOUND" | "AVOID_SELECT_STAR" | "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE" | "MISSING_WHERE_CLAUSE" | "MISSING_LIMIT_CLAUSE" | "USE_IS_NULL_NOT_EQUALS" | "AVOID_DISTINCT_WITHOUT_REASON" | "MISSING_JOIN_CONDITION" | "AVOID_LEADING_WILDCARD_LIKE" | "CONSIDER_IN_INSTEAD_OF_MANY_ORS" | "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY" | "PREFER_NOT_EXISTS_OVER_NOT_IN" | "AVOID_ORDER_BY_RANDOM" | "PREFER_FILTER_OVER_CASE_IN_AGGREGATE" | "PREFER_UNION_ALL_OVER_UNION" | "NULLS_FIRST_IN_DESC_ORDER" | "AVOID_SCALAR_SUBQUERY_IN_SELECT" | "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK" | "PREFER_COUNT_STAR_OVER_COUNT_COLUMN" | "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES" | "CONSIDER_JSONB_CONTAINMENT_OPERATOR" | "LIMIT_WITHOUT_ORDER_BY";
|
|
8
8
|
type Nudge = {
|
|
9
9
|
kind: NudgeKind;
|
|
10
10
|
severity: "CRITICAL" | "WARNING" | "INFO";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudges.d.cts","names":[],"sources":["../../src/sql/nudges.ts"],"mappings":";;;;;;KAIK,SAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"nudges.d.cts","names":[],"sources":["../../src/sql/nudges.ts"],"mappings":";;;;;;KAIK,SAAA;AAAA,KAyBO,KAAA;EACV,IAAA,EAAM,SAAA;EACN,QAAA;EACA,OAAA;EACA,QAAA;AAAA;;AAJF;;;iBAkDgB,WAAA,CACd,IAAA,EAAM,IAAA,EACN,KAAA,GAAQ,WAAA,CAAY,IAAA,gBACnB,KAAA"}
|
package/dist/sql/nudges.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { KeysOfUnion } from "./ast-utils.mjs";
|
|
|
4
4
|
import { Node } from "@pgsql/types";
|
|
5
5
|
|
|
6
6
|
//#region src/sql/nudges.d.ts
|
|
7
|
-
type NudgeKind = "LARGE_IMPROVEMENT_FOUND" | "SMALL_IMPROVEMENT_FOUND" | "AVOID_SELECT_STAR" | "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE" | "MISSING_WHERE_CLAUSE" | "MISSING_LIMIT_CLAUSE" | "USE_IS_NULL_NOT_EQUALS" | "AVOID_DISTINCT_WITHOUT_REASON" | "MISSING_JOIN_CONDITION" | "AVOID_LEADING_WILDCARD_LIKE" | "CONSIDER_IN_INSTEAD_OF_MANY_ORS" | "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY" | "PREFER_NOT_EXISTS_OVER_NOT_IN" | "AVOID_ORDER_BY_RANDOM" | "PREFER_FILTER_OVER_CASE_IN_AGGREGATE" | "PREFER_UNION_ALL_OVER_UNION" | "NULLS_FIRST_IN_DESC_ORDER" | "AVOID_SCALAR_SUBQUERY_IN_SELECT" | "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK" | "PREFER_COUNT_STAR_OVER_COUNT_COLUMN" | "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES" | "CONSIDER_JSONB_CONTAINMENT_OPERATOR";
|
|
7
|
+
type NudgeKind = "LARGE_IMPROVEMENT_FOUND" | "SMALL_IMPROVEMENT_FOUND" | "AVOID_SELECT_STAR" | "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE" | "MISSING_WHERE_CLAUSE" | "MISSING_LIMIT_CLAUSE" | "USE_IS_NULL_NOT_EQUALS" | "AVOID_DISTINCT_WITHOUT_REASON" | "MISSING_JOIN_CONDITION" | "AVOID_LEADING_WILDCARD_LIKE" | "CONSIDER_IN_INSTEAD_OF_MANY_ORS" | "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY" | "PREFER_NOT_EXISTS_OVER_NOT_IN" | "AVOID_ORDER_BY_RANDOM" | "PREFER_FILTER_OVER_CASE_IN_AGGREGATE" | "PREFER_UNION_ALL_OVER_UNION" | "NULLS_FIRST_IN_DESC_ORDER" | "AVOID_SCALAR_SUBQUERY_IN_SELECT" | "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK" | "PREFER_COUNT_STAR_OVER_COUNT_COLUMN" | "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES" | "CONSIDER_JSONB_CONTAINMENT_OPERATOR" | "LIMIT_WITHOUT_ORDER_BY";
|
|
8
8
|
type Nudge = {
|
|
9
9
|
kind: NudgeKind;
|
|
10
10
|
severity: "CRITICAL" | "WARNING" | "INFO";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudges.d.mts","names":[],"sources":["../../src/sql/nudges.ts"],"mappings":";;;;;;KAIK,SAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"nudges.d.mts","names":[],"sources":["../../src/sql/nudges.ts"],"mappings":";;;;;;KAIK,SAAA;AAAA,KAyBO,KAAA;EACV,IAAA,EAAM,SAAA;EACN,QAAA;EACA,OAAA;EACA,QAAA;AAAA;;AAJF;;;iBAkDgB,WAAA,CACd,IAAA,EAAM,IAAA,EACN,KAAA,GAAQ,WAAA,CAAY,IAAA,gBACnB,KAAA"}
|
package/dist/sql/nudges.mjs
CHANGED
|
@@ -84,6 +84,11 @@ function parseNudges(node, stack) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
if (is(node, "SelectStmt") && node.SelectStmt.limitCount && !isNullConstant(node.SelectStmt.limitCount) && !node.SelectStmt.sortClause) nudges.push({
|
|
88
|
+
kind: "LIMIT_WITHOUT_ORDER_BY",
|
|
89
|
+
severity: "WARNING",
|
|
90
|
+
message: "LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output"
|
|
91
|
+
});
|
|
87
92
|
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
|
|
88
93
|
if (!is(sortItem, "SortBy")) continue;
|
|
89
94
|
const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
|
package/dist/sql/nudges.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nudges.mjs","names":[],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"Prefer COUNT(*) over COUNT(column) or COUNT(1) — COUNT(*) counts rows without checking for NULLs. If you need to count non-NULL values, COUNT(column) is correct.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nudges.mjs","names":[],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\"\n | \"LIMIT_WITHOUT_ORDER_BY\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect LIMIT without ORDER BY (non-deterministic results)\n // LIMIT ALL parses as limitCount with A_Const.isnull — exclude it\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.limitCount &&\n !isNullConstant(node.SelectStmt.limitCount) &&\n !node.SelectStmt.sortClause\n ) {\n nudges.push({\n kind: \"LIMIT_WITHOUT_ORDER_BY\",\n severity: \"WARNING\",\n message:\n \"LIMIT without ORDER BY returns non-deterministic results — add ORDER BY for predictable output\",\n });\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"Prefer COUNT(*) over COUNT(column) or COUNT(1) — COUNT(*) counts rows without checking for NULLs. If you need to count non-NULL values, COUNT(column) is correct.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|