@query-doctor/core 0.8.1 → 0.8.2-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/_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.cjs +13 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs +13 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPrimitive.cjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPrimitive.mjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPropertyKey.cjs +10 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPropertyKey.mjs +10 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/typeof.cjs +17 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/typeof.mjs +12 -0
- package/dist/_virtual/_rolldown/runtime.cjs +24 -0
- package/dist/index.cjs +33 -2568
- package/dist/index.d.cts +11 -781
- package/dist/index.d.mts +11 -781
- package/dist/index.mjs +10 -2522
- package/dist/optimizer/genalgo.cjs +365 -0
- package/dist/optimizer/genalgo.cjs.map +1 -0
- package/dist/optimizer/genalgo.d.cts +111 -0
- package/dist/optimizer/genalgo.d.cts.map +1 -0
- package/dist/optimizer/genalgo.d.mts +111 -0
- package/dist/optimizer/genalgo.d.mts.map +1 -0
- package/dist/optimizer/genalgo.mjs +362 -0
- package/dist/optimizer/genalgo.mjs.map +1 -0
- package/dist/optimizer/pss-rewriter.cjs +31 -0
- package/dist/optimizer/pss-rewriter.cjs.map +1 -0
- package/dist/optimizer/pss-rewriter.d.cts +16 -0
- package/dist/optimizer/pss-rewriter.d.cts.map +1 -0
- package/dist/optimizer/pss-rewriter.d.mts +16 -0
- package/dist/optimizer/pss-rewriter.d.mts.map +1 -0
- package/dist/optimizer/pss-rewriter.mjs +31 -0
- package/dist/optimizer/pss-rewriter.mjs.map +1 -0
- package/dist/optimizer/statistics.cjs +738 -0
- package/dist/optimizer/statistics.cjs.map +1 -0
- package/dist/optimizer/statistics.d.cts +389 -0
- package/dist/optimizer/statistics.d.cts.map +1 -0
- package/dist/optimizer/statistics.d.mts +389 -0
- package/dist/optimizer/statistics.d.mts.map +1 -0
- package/dist/optimizer/statistics.mjs +729 -0
- package/dist/optimizer/statistics.mjs.map +1 -0
- package/dist/sentry.cjs +13 -0
- package/dist/sentry.cjs.map +1 -0
- package/dist/sentry.d.cts +7 -0
- package/dist/sentry.d.cts.map +1 -0
- package/dist/sentry.d.mts +7 -0
- package/dist/sentry.d.mts.map +1 -0
- package/dist/sentry.mjs +13 -0
- package/dist/sentry.mjs.map +1 -0
- package/dist/sql/analyzer.cjs +242 -0
- package/dist/sql/analyzer.cjs.map +1 -0
- package/dist/sql/analyzer.d.cts +112 -0
- package/dist/sql/analyzer.d.cts.map +1 -0
- package/dist/sql/analyzer.d.mts +112 -0
- package/dist/sql/analyzer.d.mts.map +1 -0
- package/dist/sql/analyzer.mjs +240 -0
- package/dist/sql/analyzer.mjs.map +1 -0
- package/dist/sql/ast-utils.cjs +19 -0
- package/dist/sql/ast-utils.cjs.map +1 -0
- package/dist/sql/ast-utils.d.cts +9 -0
- package/dist/sql/ast-utils.d.cts.map +1 -0
- package/dist/sql/ast-utils.d.mts +9 -0
- package/dist/sql/ast-utils.d.mts.map +1 -0
- package/dist/sql/ast-utils.mjs +17 -0
- package/dist/sql/ast-utils.mjs.map +1 -0
- package/dist/sql/builder.cjs +94 -0
- package/dist/sql/builder.cjs.map +1 -0
- package/dist/sql/builder.d.cts +37 -0
- package/dist/sql/builder.d.cts.map +1 -0
- package/dist/sql/builder.d.mts +37 -0
- package/dist/sql/builder.d.mts.map +1 -0
- package/dist/sql/builder.mjs +94 -0
- package/dist/sql/builder.mjs.map +1 -0
- package/dist/sql/database.cjs +35 -0
- package/dist/sql/database.cjs.map +1 -0
- package/dist/sql/database.d.cts +91 -0
- package/dist/sql/database.d.cts.map +1 -0
- package/dist/sql/database.d.mts +91 -0
- package/dist/sql/database.d.mts.map +1 -0
- package/dist/sql/database.mjs +32 -0
- package/dist/sql/database.mjs.map +1 -0
- package/dist/sql/indexes.cjs +17 -0
- package/dist/sql/indexes.cjs.map +1 -0
- package/dist/sql/indexes.d.cts +14 -0
- package/dist/sql/indexes.d.cts.map +1 -0
- package/dist/sql/indexes.d.mts +14 -0
- package/dist/sql/indexes.d.mts.map +1 -0
- package/dist/sql/indexes.mjs +16 -0
- package/dist/sql/indexes.mjs.map +1 -0
- package/dist/sql/nudges.cjs +484 -0
- package/dist/sql/nudges.cjs.map +1 -0
- package/dist/sql/nudges.d.cts +21 -0
- package/dist/sql/nudges.d.cts.map +1 -0
- package/dist/sql/nudges.d.mts +21 -0
- package/dist/sql/nudges.d.mts.map +1 -0
- package/dist/sql/nudges.mjs +484 -0
- package/dist/sql/nudges.mjs.map +1 -0
- package/dist/sql/permutations.cjs +28 -0
- package/dist/sql/permutations.cjs.map +1 -0
- package/dist/sql/permutations.mjs +28 -0
- package/dist/sql/permutations.mjs.map +1 -0
- package/dist/sql/pg-identifier.cjs +216 -0
- package/dist/sql/pg-identifier.cjs.map +1 -0
- package/dist/sql/pg-identifier.d.cts +31 -0
- package/dist/sql/pg-identifier.d.cts.map +1 -0
- package/dist/sql/pg-identifier.d.mts +31 -0
- package/dist/sql/pg-identifier.d.mts.map +1 -0
- package/dist/sql/pg-identifier.mjs +216 -0
- package/dist/sql/pg-identifier.mjs.map +1 -0
- package/dist/sql/walker.cjs +314 -0
- package/dist/sql/walker.cjs.map +1 -0
- package/dist/sql/walker.d.cts +15 -0
- package/dist/sql/walker.d.cts.map +1 -0
- package/dist/sql/walker.d.mts +15 -0
- package/dist/sql/walker.d.mts.map +1 -0
- package/dist/sql/walker.mjs +313 -0
- package/dist/sql/walker.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +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\";\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 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\nconst DEFAULT_RELTUPLES = 10_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 class Statistics {\n readonly mode: StatisticsMode;\n private readonly exportedMetadata: ExportedStats[] | undefined;\n // preventing accidental internal mutations\n static readonly defaultStatsMode: StatisticsMode = Object.freeze({\n kind: \"fromAssumption\",\n reltuples: DEFAULT_RELTUPLES,\n relpages: DEFAULT_RELPAGES,\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 }\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, \"full\");\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 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 const processedTables = new Set<string>();\n\n let columnStatsUpdatePromise: Promise<any> | undefined;\n const columnStatsValues: Array<{\n schema_name: string;\n table_name: string;\n column_name: string;\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: string;\n staop2: string;\n staop3: string;\n staop4: string;\n staop5: string;\n stacoll1: string;\n stacoll2: string;\n stacoll3: string;\n stacoll4: string;\n stacoll5: string;\n stanumbers1: number[] | null;\n stanumbers2: number[] | null;\n stanumbers3: number[] | null;\n stanumbers4: number[] | null;\n stanumbers5: number[] | null;\n stavalues1: any[] | null;\n stavalues2: any[] | null;\n stavalues3: any[] | null;\n stavalues4: any[] | null;\n stavalues5: any[] | null;\n _value_type1: StaValueKind;\n _value_type2: StaValueKind;\n _value_type3: StaValueKind;\n _value_type4: StaValueKind;\n _value_type5: StaValueKind;\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 for (const column of target) {\n const { stats } = column;\n // setting baseline numbers to make sure columns arent\n // affected by a lack of column stats\n if (!stats || this.mode.kind === \"fromAssumption\") {\n const stawidth = stats?.stawidth || estimateStawidth(column);\n columnStatsValues.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\n stainherit: false,\n // 4% nulls\n stanullfrac: 0.04,\n // use the existing stats (if possible) 32bytes per row if not\n stawidth,\n // 90% distinct fields. We really want to be able to\n // finely control this field eventually but good enough for now\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 // setting some kind of correlation to make index lookups\n // a little bit more favorable\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 // TODO: should this depend on the real table's data types?\n // Right now we're mixing the concerns of stats with schema so\n // one isn't available without the other.\n _value_type1: \"real\",\n _value_type2: \"real\",\n _value_type3: \"real\",\n _value_type4: \"real\",\n _value_type5: \"real\",\n });\n continue;\n }\n // TODO: track processed columns too\n columnStatsValues.push({\n schema_name: table.schemaName,\n table_name: table.tableName,\n column_name: column.columnName,\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 * 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 sql = 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 columnStatsUpdatePromise = tx\n .exec(sql, [columnStatsValues])\n .catch((err) => {\n console.error(\"Something wrong wrong updating column stats\");\n console.error(err);\n throw err;\n // return err;\n // return Promise.reject(err)\n });\n\n const reltuplesValues: Array<{\n reltuples: number;\n relpages: number;\n relname: string;\n schema_name: string;\n relallvisible: number;\n relallfrozen?: number;\n }> = [];\n for (const table of this.ownMetadata) {\n processedTables.add(`${table.schemaName}.${table.tableName}`);\n let targetTable: ExportedStats | undefined;\n if (this.exportedMetadata) {\n targetTable = this.exportedMetadata.find(\n (m) =>\n m.tableName === table.tableName &&\n m.schemaName === table.schemaName,\n );\n } else {\n targetTable = table;\n }\n let reltuples: number;\n let relpages: number;\n let relallvisible: number = 0;\n let relallfrozen: number | undefined;\n if (this.mode.kind === \"fromAssumption\") {\n reltuples = this.mode.reltuples;\n relpages = estimateRelpages(reltuples, table.columns);\n } else if (targetTable) {\n // don't want to run our prod stats with -1 reltuples\n // we warn the user about this later\n // if (targetTable.reltuples < 10 || targetTable.reltuples > 10000) {\n reltuples = targetTable.reltuples;\n relpages = targetTable.relpages;\n relallvisible = targetTable.relallvisible;\n relallfrozen = targetTable.relallfrozen;\n // }\n } else {\n // we want to warn about tables that are in the test but not in the exported stats\n // this can happen in case a new table is created in a PR\n warnings.tablesNotInExports.push(\n `${table.schemaName}.${table.tableName}`,\n );\n reltuples = DEFAULT_RELTUPLES;\n relpages = DEFAULT_RELPAGES;\n }\n reltuplesValues.push({\n relname: table.tableName,\n schema_name: table.schemaName,\n reltuples,\n relpages,\n relallfrozen,\n relallvisible,\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 relpages,\n );\n reltuplesValues.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 reltuplesValues.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 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 reltuplesPromise = tx\n .exec(reltuplesQuery, [reltuplesValues])\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 // we expect production stats to have real numbers\n warnings.tableNotAnalyzed.push(\n `${table.schemaName}.${table.tableName}`,\n );\n }\n if (tableExists) {\n continue;\n }\n // there's a LOT of tables in statistics exports for things like timescaledb\n // that might not show up in the test data. This check might be too strict.\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 === columnStatsValues.length\n : true;\n if (!updatedColumnsProperly) {\n console.error(`Did not update expected column stats`);\n }\n if (reltuplesUpdates.length !== reltuplesValues.length) {\n console.error(`Did not update expected reltuples/relpages`);\n }\n return warnings;\n }\n\n static async dumpStats(\n db: PostgresTransaction,\n postgresVersion: PostgresVersion,\n kind: \"anonymous\" | \"full\",\n ): Promise<ExportedStats[]> {\n const fullDump = kind === \"full\";\n console.log(`dumping stats for postgres ${gray(postgresVersion)}`);\n // certain things are only supported with pg17\n const stats = await db.exec<{ json_agg: ExportedStats[] }>(\n `\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 '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', CASE WHEN $1 THEN s.stavalues1 ELSE NULL END,\n 'stavalues2', CASE WHEN $1 THEN s.stavalues2 ELSE NULL END,\n 'stavalues3', CASE WHEN $1 THEN s.stavalues3 ELSE NULL END,\n 'stavalues4', CASE WHEN $1 THEN s.stavalues4 ELSE NULL END,\n 'stavalues5', CASE WHEN $1 THEN s.stavalues5 ELSE NULL END\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 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 [fullDump],\n );\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/sentry.cjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/sentry.ts
|
|
3
|
+
function deriveSentryEnvironment(url) {
|
|
4
|
+
if (!url) return "local";
|
|
5
|
+
if (url.includes("next")) return "next";
|
|
6
|
+
if (url.includes("staging")) return "staging";
|
|
7
|
+
if (url.includes("querydoctor.com")) return "production";
|
|
8
|
+
return "local";
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
exports.deriveSentryEnvironment = deriveSentryEnvironment;
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=sentry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.cjs","names":[],"sources":["../src/sentry.ts"],"sourcesContent":["export function deriveSentryEnvironment(url: string | undefined): string {\n if (!url) return \"local\";\n if (url.includes(\"next\")) return \"next\";\n if (url.includes(\"staging\")) return \"staging\";\n if (url.includes(\"querydoctor.com\")) return \"production\";\n return \"local\";\n}\n"],"mappings":";;AAAA,SAAgB,wBAAwB,KAAiC;AACvE,KAAI,CAAC,IAAK,QAAO;AACjB,KAAI,IAAI,SAAS,OAAO,CAAE,QAAO;AACjC,KAAI,IAAI,SAAS,UAAU,CAAE,QAAO;AACpC,KAAI,IAAI,SAAS,kBAAkB,CAAE,QAAO;AAC5C,QAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.d.cts","names":[],"sources":["../src/sentry.ts"],"mappings":";;;iBAAgB,uBAAA,CAAwB,GAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.d.mts","names":[],"sources":["../src/sentry.ts"],"mappings":";;;iBAAgB,uBAAA,CAAwB,GAAA"}
|
package/dist/sentry.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/sentry.ts
|
|
3
|
+
function deriveSentryEnvironment(url) {
|
|
4
|
+
if (!url) return "local";
|
|
5
|
+
if (url.includes("next")) return "next";
|
|
6
|
+
if (url.includes("staging")) return "staging";
|
|
7
|
+
if (url.includes("querydoctor.com")) return "production";
|
|
8
|
+
return "local";
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { deriveSentryEnvironment };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=sentry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.mjs","names":[],"sources":["../src/sentry.ts"],"sourcesContent":["export function deriveSentryEnvironment(url: string | undefined): string {\n if (!url) return \"local\";\n if (url.includes(\"next\")) return \"next\";\n if (url.includes(\"staging\")) return \"staging\";\n if (url.includes(\"querydoctor.com\")) return \"production\";\n return \"local\";\n}\n"],"mappings":";;AAAA,SAAgB,wBAAwB,KAAiC;AACvE,KAAI,CAAC,IAAK,QAAO;AACjB,KAAI,IAAI,SAAS,OAAO,CAAE,QAAO;AACjC,KAAI,IAAI,SAAS,UAAU,CAAE,QAAO;AACpC,KAAI,IAAI,SAAS,kBAAkB,CAAE,QAAO;AAC5C,QAAO"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
require("../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_ast_utils = require("./ast-utils.cjs");
|
|
4
|
+
const require_walker = require("./walker.cjs");
|
|
5
|
+
let colorette = require("colorette");
|
|
6
|
+
//#region src/sql/analyzer.ts
|
|
7
|
+
const ignoredIdentifier = "__qd_placeholder";
|
|
8
|
+
const STATEMENT_TYPE_MAP = {
|
|
9
|
+
SelectStmt: "select",
|
|
10
|
+
InsertStmt: "insert",
|
|
11
|
+
UpdateStmt: "update",
|
|
12
|
+
DeleteStmt: "delete"
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes a query and returns a list of column references that
|
|
16
|
+
* should be indexed.
|
|
17
|
+
*
|
|
18
|
+
* This should be instantiated once per analyzed query.
|
|
19
|
+
*/
|
|
20
|
+
var Analyzer = class {
|
|
21
|
+
constructor(parser) {
|
|
22
|
+
this.parser = parser;
|
|
23
|
+
}
|
|
24
|
+
async analyze(query, formattedQuery) {
|
|
25
|
+
const ast = await this.parser(query);
|
|
26
|
+
if (!ast.stmts) throw new Error("Query did not have any statements. This should probably never happen?");
|
|
27
|
+
const stmt = ast.stmts[0].stmt;
|
|
28
|
+
if (!stmt) throw new Error("Query did not have any statements. This should probably never happen?");
|
|
29
|
+
const statementType = STATEMENT_TYPE_MAP[require_ast_utils.getNodeKind(stmt)] ?? "other";
|
|
30
|
+
const { highlights, indexRepresentations, indexesToCheck, shadowedAliases, tempTables, tableMappings, nudges } = new require_walker.Walker(query).walk(stmt);
|
|
31
|
+
const sortedHighlights = highlights.sort((a, b) => b.position.end - a.position.end);
|
|
32
|
+
let currQuery = query;
|
|
33
|
+
for (const highlight of sortedHighlights) {
|
|
34
|
+
const parts = this.resolveTableAliases(highlight.parts, tableMappings);
|
|
35
|
+
if (parts.length === 0) {
|
|
36
|
+
console.error(highlight);
|
|
37
|
+
throw new Error("Highlight must have at least one part");
|
|
38
|
+
}
|
|
39
|
+
let color;
|
|
40
|
+
let skip = false;
|
|
41
|
+
if (highlight.ignored) {
|
|
42
|
+
color = (x) => (0, colorette.dim)((0, colorette.strikethrough)(x));
|
|
43
|
+
skip = true;
|
|
44
|
+
} else if (parts.length === 2 && tempTables.has(parts[0].text) && !tableMappings.has(parts[0].text)) {
|
|
45
|
+
color = colorette.blue;
|
|
46
|
+
skip = true;
|
|
47
|
+
} else color = colorette.bgMagentaBright;
|
|
48
|
+
const queryRepr = highlight.representation;
|
|
49
|
+
const queryBeforeMatch = currQuery.slice(0, highlight.position.start);
|
|
50
|
+
const queryAfterToken = currQuery.slice(highlight.position.end);
|
|
51
|
+
currQuery = `${queryBeforeMatch}${color(queryRepr)}${this.colorizeKeywords(queryAfterToken, color)}`;
|
|
52
|
+
const reprKey = highlight.jsonbExtraction ? `${queryRepr}::${highlight.jsonbExtraction}` : queryRepr;
|
|
53
|
+
if (indexRepresentations.has(reprKey)) skip = true;
|
|
54
|
+
if (!skip) {
|
|
55
|
+
indexesToCheck.push(highlight);
|
|
56
|
+
indexRepresentations.add(reprKey);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const referencedTables = [];
|
|
60
|
+
for (const value of tableMappings.values()) if (!value.alias) referencedTables.push({
|
|
61
|
+
schema: value.schema,
|
|
62
|
+
table: value.text
|
|
63
|
+
});
|
|
64
|
+
const { tags, queryWithoutTags } = this.extractSqlcommenter(query);
|
|
65
|
+
const formattedQueryWithoutTags = formattedQuery ? this.extractSqlcommenter(formattedQuery).queryWithoutTags : void 0;
|
|
66
|
+
return {
|
|
67
|
+
statementType,
|
|
68
|
+
indexesToCheck,
|
|
69
|
+
ansiHighlightedQuery: currQuery,
|
|
70
|
+
referencedTables,
|
|
71
|
+
shadowedAliases,
|
|
72
|
+
tags,
|
|
73
|
+
queryWithoutTags,
|
|
74
|
+
formattedQueryWithoutTags,
|
|
75
|
+
nudges
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
deriveIndexes(tables, discovered, referencedTables) {
|
|
79
|
+
/**
|
|
80
|
+
* There are 3 different kinds of parts a col reference can have
|
|
81
|
+
* {a} = just a column within context. Find out the table
|
|
82
|
+
* {a, b} = a column reference with a table reference. There's still ambiguity here
|
|
83
|
+
* with what the schema could be in case there are 2 tables with the same name in different schemas.
|
|
84
|
+
* {a, b, c} = a column reference with a table reference and a schema reference.
|
|
85
|
+
* This is the best case scenario.
|
|
86
|
+
*/
|
|
87
|
+
const allIndexes = [];
|
|
88
|
+
const seenIndexes = /* @__PURE__ */ new Set();
|
|
89
|
+
function addIndex(index) {
|
|
90
|
+
const extractionSuffix = index.jsonbExtraction ? `:"${index.jsonbExtraction}"` : "";
|
|
91
|
+
const key = `"${index.schema}":"${index.table}":"${index.column}"${extractionSuffix}`;
|
|
92
|
+
if (seenIndexes.has(key)) return;
|
|
93
|
+
seenIndexes.add(key);
|
|
94
|
+
allIndexes.push(index);
|
|
95
|
+
}
|
|
96
|
+
const matchingTables = this.filterReferences(referencedTables, tables);
|
|
97
|
+
for (const colReference of discovered) {
|
|
98
|
+
const partsCount = colReference.parts.length;
|
|
99
|
+
const columnOnlyReference = partsCount === 1;
|
|
100
|
+
const tableReference = partsCount === 2;
|
|
101
|
+
const fullReference = partsCount === 3;
|
|
102
|
+
if (columnOnlyReference) {
|
|
103
|
+
const [column] = colReference.parts;
|
|
104
|
+
const referencedColumn = this.normalize(column);
|
|
105
|
+
for (const table of matchingTables) {
|
|
106
|
+
if (!this.hasColumn(table, referencedColumn)) continue;
|
|
107
|
+
const index = {
|
|
108
|
+
schema: table.schemaName,
|
|
109
|
+
table: table.tableName,
|
|
110
|
+
column: referencedColumn
|
|
111
|
+
};
|
|
112
|
+
if (colReference.sort) index.sort = colReference.sort;
|
|
113
|
+
if (colReference.where) index.where = colReference.where;
|
|
114
|
+
if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
|
|
115
|
+
if (colReference.jsonbExtraction) index.jsonbExtraction = colReference.jsonbExtraction;
|
|
116
|
+
addIndex(index);
|
|
117
|
+
}
|
|
118
|
+
} else if (tableReference) {
|
|
119
|
+
const [table, column] = colReference.parts;
|
|
120
|
+
const referencedTable = this.normalize(table);
|
|
121
|
+
const referencedColumn = this.normalize(column);
|
|
122
|
+
for (const matchingTable of matchingTables) {
|
|
123
|
+
if (!this.hasColumn(matchingTable, referencedColumn)) continue;
|
|
124
|
+
const index = {
|
|
125
|
+
schema: matchingTable.schemaName,
|
|
126
|
+
table: referencedTable,
|
|
127
|
+
column: referencedColumn
|
|
128
|
+
};
|
|
129
|
+
if (colReference.sort) index.sort = colReference.sort;
|
|
130
|
+
if (colReference.where) index.where = colReference.where;
|
|
131
|
+
if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
|
|
132
|
+
if (colReference.jsonbExtraction) index.jsonbExtraction = colReference.jsonbExtraction;
|
|
133
|
+
addIndex(index);
|
|
134
|
+
}
|
|
135
|
+
} else if (fullReference) {
|
|
136
|
+
const [schema, table, column] = colReference.parts;
|
|
137
|
+
const index = {
|
|
138
|
+
schema: this.normalize(schema),
|
|
139
|
+
table: this.normalize(table),
|
|
140
|
+
column: this.normalize(column)
|
|
141
|
+
};
|
|
142
|
+
if (colReference.sort) index.sort = colReference.sort;
|
|
143
|
+
if (colReference.where) index.where = colReference.where;
|
|
144
|
+
if (colReference.jsonbOperator) index.jsonbOperator = colReference.jsonbOperator;
|
|
145
|
+
if (colReference.jsonbExtraction) index.jsonbExtraction = colReference.jsonbExtraction;
|
|
146
|
+
addIndex(index);
|
|
147
|
+
} else {
|
|
148
|
+
console.error("Column reference has too many parts. The query is malformed", colReference);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return allIndexes;
|
|
153
|
+
}
|
|
154
|
+
filterReferences(referencedTables, tables) {
|
|
155
|
+
const matchingTables = [];
|
|
156
|
+
for (const referencedTable of referencedTables) {
|
|
157
|
+
const refs = tables.filter(({ tableName, schemaName }) => {
|
|
158
|
+
let schemaMatches = true;
|
|
159
|
+
if (referencedTable.schema) schemaMatches = schemaName === referencedTable.schema;
|
|
160
|
+
return schemaMatches && tableName === referencedTable.table;
|
|
161
|
+
});
|
|
162
|
+
matchingTables.push(...refs);
|
|
163
|
+
}
|
|
164
|
+
return matchingTables;
|
|
165
|
+
}
|
|
166
|
+
hasColumn(table, columnName) {
|
|
167
|
+
return table.columns?.some((column) => column.columnName === columnName) ?? false;
|
|
168
|
+
}
|
|
169
|
+
colorizeKeywords(query, color) {
|
|
170
|
+
return query.replace(/(^\s+)(asc|desc)?(\s+(nulls first|nulls last))?/i, (_, pre, dir, spaceNulls, nulls) => {
|
|
171
|
+
return `${pre}${dir ? color(dir) : ""}${nulls ? spaceNulls.replace(nulls, color(nulls)) : ""}`;
|
|
172
|
+
}).replace(/(^\s+)(is (null|not null))/i, (_, pre, nulltest) => {
|
|
173
|
+
return `${pre}${color(nulltest)}`;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolves aliases such as `a.b` to `x.b` if `a` is a known
|
|
178
|
+
* alias to a table called x.
|
|
179
|
+
*
|
|
180
|
+
* Ignores all other combination of parts such as `a.b.c`
|
|
181
|
+
*/
|
|
182
|
+
resolveTableAliases(parts, tableMappings) {
|
|
183
|
+
if (parts.length !== 2) return parts;
|
|
184
|
+
const tablePart = parts[0];
|
|
185
|
+
const mapping = tableMappings.get(tablePart.text);
|
|
186
|
+
if (mapping) parts[0] = mapping;
|
|
187
|
+
return parts;
|
|
188
|
+
}
|
|
189
|
+
normalize(columnReference) {
|
|
190
|
+
return columnReference.quoted ? columnReference.text : columnReference.text.toLowerCase();
|
|
191
|
+
}
|
|
192
|
+
extractSqlcommenter(query) {
|
|
193
|
+
const trimmedQuery = query.trimEnd();
|
|
194
|
+
const startPosition = trimmedQuery.lastIndexOf("/*");
|
|
195
|
+
const endPosition = trimmedQuery.lastIndexOf("*/");
|
|
196
|
+
if (startPosition === -1 || endPosition === -1) return {
|
|
197
|
+
tags: [],
|
|
198
|
+
queryWithoutTags: trimmedQuery
|
|
199
|
+
};
|
|
200
|
+
const afterComment = trimmedQuery.slice(endPosition + 2).trim();
|
|
201
|
+
if (afterComment && afterComment !== ";") return {
|
|
202
|
+
tags: [],
|
|
203
|
+
queryWithoutTags: trimmedQuery
|
|
204
|
+
};
|
|
205
|
+
const queryWithoutTags = trimmedQuery.slice(0, startPosition);
|
|
206
|
+
const tagString = trimmedQuery.slice(startPosition + 2, endPosition).trim();
|
|
207
|
+
if (!tagString || typeof tagString !== "string") return {
|
|
208
|
+
tags: [],
|
|
209
|
+
queryWithoutTags
|
|
210
|
+
};
|
|
211
|
+
const tags = [];
|
|
212
|
+
for (const match of tagString.split(",")) {
|
|
213
|
+
const [key, value] = match.split("=");
|
|
214
|
+
if (!key || !value) {
|
|
215
|
+
if (tags.length > 0) console.warn(`Invalid sqlcommenter tag: ${match} in comment: ${tagString}. Ignoring`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
let sliceStart = 0;
|
|
220
|
+
if (value.startsWith("'")) sliceStart = 1;
|
|
221
|
+
let sliceEnd = value.length;
|
|
222
|
+
if (value.endsWith("'")) sliceEnd -= 1;
|
|
223
|
+
const decoded = decodeURIComponent(value.slice(sliceStart, sliceEnd));
|
|
224
|
+
tags.push({
|
|
225
|
+
key: key.trim(),
|
|
226
|
+
value: decoded
|
|
227
|
+
});
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error(err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
tags,
|
|
234
|
+
queryWithoutTags
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
exports.Analyzer = Analyzer;
|
|
240
|
+
exports.ignoredIdentifier = ignoredIdentifier;
|
|
241
|
+
|
|
242
|
+
//# sourceMappingURL=analyzer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.cjs","names":["getNodeKind","Walker","blue","bgMagentaBright"],"sources":["../../src/sql/analyzer.ts"],"sourcesContent":["import type {\n NullTestType,\n ParseResult,\n SortByDir,\n SortByNulls,\n} from \"@pgsql/types\";\nimport {\n bgMagentaBright,\n blue,\n type Color,\n dim,\n strikethrough,\n} from \"colorette\";\nimport type { RootIndexCandidate } from \"../optimizer/genalgo.js\";\nimport type { ExportedStats } from \"../optimizer/statistics.js\";\nimport { getNodeKind } from \"./ast-utils.js\";\nimport type { Nudge } from \"./nudges.js\";\nimport { ColumnReferencePart, TableMappings, Walker } from \"./walker.js\";\n\nexport interface DatabaseDriver {\n query(query: string, params: unknown[]): Promise<unknown[]>;\n}\n\nexport const ignoredIdentifier = \"__qd_placeholder\";\n\nexport interface SQLCommenterTag {\n key: string;\n value: string;\n}\n\nexport type SortContext = {\n dir: SortByDir;\n nulls: SortByNulls;\n};\n\nexport type DiscoveredColumnReference = {\n /** How often the column reference appears in the query. */\n frequency: number;\n /**\n * Representation of the column reference exactly\n * as it appears in the query.\n */\n representation: string;\n /**\n * Parts of the column reference separated by dots in the query.\n * The table reference (if it exists) is resolved if the query\n * uses an alias.\n *\n * Has 3 different potential configurations (in theory)\n * `a.b.c` - a column reference with a table and a schema reference\n * `a.b` - a column reference with a table reference but no schema\n * `a` - a column reference with no table reference.\n *\n * We use a simple array here to allow parsing of any syntactically correct\n * but logically incorrect query. The checks happen later when we're deriving\n * potential indexes from parts of a column reference in `deriveIndexes`\n */\n parts: ColumnReferencePart[];\n /**\n * Whether the column reference is invalid. This\n */\n ignored: boolean;\n /** The position of the column reference in the query. */\n position: {\n start: number;\n end: number;\n };\n /**\n * A sort direction associated by the column reference.\n * Only relevant to references from sorts\n */\n sort?: SortContext;\n where?: { nulltest?: NullTestType };\n jsonbOperator?: JsonbOperator;\n jsonbExtraction?: string;\n};\n\nexport type JsonbOperator = \"@>\" | \"?\" | \"?|\" | \"?&\" | \"@@\" | \"@?\";\n\nexport type StatementType = \"select\" | \"insert\" | \"update\" | \"delete\" | \"other\";\n\nconst STATEMENT_TYPE_MAP: Record<string, StatementType> = {\n SelectStmt: \"select\",\n InsertStmt: \"insert\",\n UpdateStmt: \"update\",\n DeleteStmt: \"delete\",\n};\n\n/** A function defined by @pgsql/parser */\nexport type Parser = (query: string) => Promise<unknown>;\n\nexport type TableReference = {\n schema?: string;\n table: string;\n};\n\nexport type AnalysisResult = {\n statementType: StatementType;\n indexesToCheck: DiscoveredColumnReference[];\n ansiHighlightedQuery: string;\n referencedTables: TableReference[];\n shadowedAliases: ColumnReferencePart[];\n tags: SQLCommenterTag[];\n queryWithoutTags: string;\n formattedQueryWithoutTags?: string;\n nudges: Nudge[];\n};\n\nexport type SQLCommenterExtraction = {\n tags: SQLCommenterTag[];\n queryWithoutTags: string;\n};\n\n/**\n * Analyzes a query and returns a list of column references that\n * should be indexed.\n *\n * This should be instantiated once per analyzed query.\n */\nexport class Analyzer {\n constructor(private readonly parser: Parser) {}\n async analyze(\n query: string,\n formattedQuery?: string,\n ): Promise<AnalysisResult> {\n const ast = (await this.parser(query)) as ParseResult;\n if (!ast.stmts) {\n throw new Error(\n \"Query did not have any statements. This should probably never happen?\",\n );\n }\n const stmt = ast.stmts[0].stmt;\n if (!stmt) {\n throw new Error(\n \"Query did not have any statements. This should probably never happen?\",\n );\n }\n const stmtKind = getNodeKind(stmt);\n const statementType: StatementType =\n STATEMENT_TYPE_MAP[stmtKind] ?? \"other\";\n const walker = new Walker(query);\n const {\n highlights,\n indexRepresentations,\n indexesToCheck,\n shadowedAliases,\n tempTables,\n tableMappings,\n nudges,\n } = walker.walk(stmt);\n const sortedHighlights = highlights.sort(\n (a, b) => b.position.end - a.position.end,\n );\n let currQuery = query;\n for (const highlight of sortedHighlights) {\n // our parts might have\n const parts = this.resolveTableAliases(highlight.parts, tableMappings);\n if (parts.length === 0) {\n console.error(highlight);\n throw new Error(\"Highlight must have at least one part\");\n }\n let color: Color;\n let skip = false;\n if (highlight.ignored) {\n color = (x) => dim(strikethrough(x));\n skip = true;\n } else if (\n parts.length === 2 &&\n tempTables.has(parts[0].text) &&\n // sometimes temp tables are aliased as existing tables\n // we don't want to ignore them if they are\n !tableMappings.has(parts[0].text)\n ) {\n color = blue;\n skip = true;\n } else {\n color = bgMagentaBright;\n }\n const queryRepr = highlight.representation;\n const queryBeforeMatch = currQuery.slice(0, highlight.position.start);\n const queryAfterToken = currQuery.slice(highlight.position.end);\n currQuery = `${queryBeforeMatch}${color(queryRepr)}${this.colorizeKeywords(\n queryAfterToken,\n color,\n )}`;\n const reprKey = highlight.jsonbExtraction\n ? `${queryRepr}::${highlight.jsonbExtraction}`\n : queryRepr;\n if (indexRepresentations.has(reprKey)) {\n skip = true;\n }\n if (!skip) {\n indexesToCheck.push(highlight);\n indexRepresentations.add(reprKey);\n }\n }\n\n const referencedTables: TableReference[] = [];\n for (const value of tableMappings.values()) {\n // aliased mappings are not concrete tables\n // eg: select * from table t -> t is not a table\n if (!value.alias) {\n referencedTables.push({\n schema: value.schema,\n table: value.text,\n });\n }\n }\n const { tags, queryWithoutTags } = this.extractSqlcommenter(query);\n\n const formattedQueryWithoutTags = formattedQuery\n ? this.extractSqlcommenter(formattedQuery).queryWithoutTags\n : undefined;\n\n return {\n statementType,\n indexesToCheck,\n ansiHighlightedQuery: currQuery,\n referencedTables,\n shadowedAliases,\n tags,\n queryWithoutTags,\n formattedQueryWithoutTags,\n nudges,\n };\n }\n\n deriveIndexes(\n tables: ExportedStats[],\n discovered: DiscoveredColumnReference[],\n referencedTables: TableReference[],\n ): RootIndexCandidate[] {\n /**\n * There are 3 different kinds of parts a col reference can have\n * {a} = just a column within context. Find out the table\n * {a, b} = a column reference with a table reference. There's still ambiguity here\n * with what the schema could be in case there are 2 tables with the same name in different schemas.\n * {a, b, c} = a column reference with a table reference and a schema reference.\n * This is the best case scenario.\n */\n const allIndexes: RootIndexCandidate[] = [];\n const seenIndexes = new Set<string>();\n function addIndex(index: RootIndexCandidate) {\n const extractionSuffix = index.jsonbExtraction\n ? `:\"${index.jsonbExtraction}\"`\n : \"\";\n const key = `\"${index.schema}\":\"${index.table}\":\"${index.column}\"${extractionSuffix}`;\n if (seenIndexes.has(key)) {\n return;\n }\n seenIndexes.add(key);\n allIndexes.push(index);\n }\n const matchingTables = this.filterReferences(referencedTables, tables);\n for (const colReference of discovered) {\n const partsCount = colReference.parts.length;\n const columnOnlyReference = partsCount === 1;\n const tableReference = partsCount === 2;\n const fullReference = partsCount === 3;\n if (columnOnlyReference) {\n // select c from x\n const [column] = colReference.parts;\n const referencedColumn = this.normalize(column);\n for (const table of matchingTables) {\n if (!this.hasColumn(table, referencedColumn)) {\n continue;\n }\n const index: RootIndexCandidate = {\n schema: table.schemaName,\n table: table.tableName,\n column: referencedColumn,\n };\n if (colReference.sort) {\n index.sort = colReference.sort;\n }\n if (colReference.where) {\n index.where = colReference.where;\n }\n if (colReference.jsonbOperator) {\n index.jsonbOperator = colReference.jsonbOperator;\n }\n if (colReference.jsonbExtraction) {\n index.jsonbExtraction = colReference.jsonbExtraction;\n }\n addIndex(index);\n }\n } else if (tableReference) {\n // select b.c from x\n const [table, column] = colReference.parts;\n const referencedTable = this.normalize(table);\n const referencedColumn = this.normalize(column);\n for (const matchingTable of matchingTables) {\n if (!this.hasColumn(matchingTable, referencedColumn)) {\n continue;\n }\n const index: RootIndexCandidate = {\n schema: matchingTable.schemaName,\n table: referencedTable,\n column: referencedColumn,\n };\n if (colReference.sort) {\n index.sort = colReference.sort;\n }\n if (colReference.where) {\n index.where = colReference.where;\n }\n if (colReference.jsonbOperator) {\n index.jsonbOperator = colReference.jsonbOperator;\n }\n if (colReference.jsonbExtraction) {\n index.jsonbExtraction = colReference.jsonbExtraction;\n }\n addIndex(index);\n }\n } else if (fullReference) {\n // select a.b.c from x\n const [schema, table, column] = colReference.parts;\n const referencedSchema = this.normalize(schema);\n const referencedTable = this.normalize(table);\n const referencedColumn = this.normalize(column);\n const index: RootIndexCandidate = {\n schema: referencedSchema,\n table: referencedTable,\n column: referencedColumn,\n };\n if (colReference.sort) {\n index.sort = colReference.sort;\n }\n if (colReference.where) {\n index.where = colReference.where;\n }\n if (colReference.jsonbOperator) {\n index.jsonbOperator = colReference.jsonbOperator;\n }\n if (colReference.jsonbExtraction) {\n index.jsonbExtraction = colReference.jsonbExtraction;\n }\n addIndex(index);\n } else {\n // select huh.a.b.c from x\n console.error(\n \"Column reference has too many parts. The query is malformed\",\n colReference,\n );\n continue;\n }\n }\n return allIndexes;\n }\n\n private filterReferences(\n referencedTables: TableReference[],\n tables: ExportedStats[],\n ): ExportedStats[] {\n const matchingTables: ExportedStats[] = [];\n for (const referencedTable of referencedTables) {\n const refs = tables.filter(({ tableName, schemaName }) => {\n // not every referenced table carries a schema with it\n let schemaMatches = true;\n if (referencedTable.schema) {\n schemaMatches = schemaName === referencedTable.schema;\n }\n return schemaMatches && tableName === referencedTable.table;\n });\n matchingTables.push(...refs);\n }\n return matchingTables;\n }\n\n private hasColumn(table: ExportedStats, columnName: string): boolean {\n return (\n table.columns?.some((column) => column.columnName === columnName) ?? false\n );\n }\n\n private colorizeKeywords(query: string, color: Color) {\n return query\n .replace(\n // eh? This kinda sucks\n /(^\\s+)(asc|desc)?(\\s+(nulls first|nulls last))?/i,\n (_, pre, dir, spaceNulls, nulls) => {\n return `${pre}${dir ? color(dir) : \"\"}${\n nulls ? spaceNulls.replace(nulls, color(nulls)) : \"\"\n }`;\n },\n )\n .replace(/(^\\s+)(is (null|not null))/i, (_, pre, nulltest) => {\n return `${pre}${color(nulltest)}`;\n });\n }\n\n /**\n * Resolves aliases such as `a.b` to `x.b` if `a` is a known\n * alias to a table called x.\n *\n * Ignores all other combination of parts such as `a.b.c`\n */\n private resolveTableAliases(\n parts: ColumnReferencePart[],\n tableMappings: TableMappings,\n ): ColumnReferencePart[] {\n // we don't want to resolve aliases for references such as\n // `a.b.c` - this is fully qualified with a schema and can't be an alias\n // `c` - because there's no table reference here (as far as we can tell)\n if (parts.length !== 2) {\n return parts;\n }\n const tablePart = parts[0];\n const mapping = tableMappings.get(tablePart.text);\n if (mapping) {\n parts[0] = mapping;\n }\n return parts;\n }\n\n private normalize(columnReference: ColumnReferencePart): string {\n return columnReference.quoted\n ? columnReference.text\n : // postgres automatically lowercases column names if not quoted\n columnReference.text.toLowerCase();\n }\n\n private extractSqlcommenter(query: string): SQLCommenterExtraction {\n const trimmedQuery = query.trimEnd();\n const startPosition = trimmedQuery.lastIndexOf(\"/*\");\n const endPosition = trimmedQuery.lastIndexOf(\"*/\");\n if (startPosition === -1 || endPosition === -1) {\n return { tags: [], queryWithoutTags: trimmedQuery };\n }\n // Only treat as SQLCommenter if the comment is at the end of the query.\n // pg_stat_statements (PG 18+) puts /*, ... */ inside IN clauses —\n // those have SQL after the closing */, so skip them.\n const afterComment = trimmedQuery.slice(endPosition + 2).trim();\n if (afterComment && afterComment !== \";\") {\n return { tags: [], queryWithoutTags: trimmedQuery };\n }\n const queryWithoutTags = trimmedQuery.slice(0, startPosition);\n const tagString = trimmedQuery.slice(startPosition + 2, endPosition).trim();\n if (!tagString || typeof tagString !== \"string\") {\n return { tags: [], queryWithoutTags: queryWithoutTags };\n }\n const tags: SQLCommenterTag[] = [];\n for (const match of tagString.split(\",\")) {\n const [key, value] = match.split(\"=\");\n // just because a comment has a `,` but not a `=` in it doesn't\n // mean that it's a malformed sqlcommenter tag. It might just be\n // a long comment with good punctuation.\n if (!key || !value) {\n // however, if there was a previously valid tag, the comment\n // is more likely to be a malformed sqlcommenter tag\n if (tags.length > 0) {\n console.warn(\n `Invalid sqlcommenter tag: ${match} in comment: ${tagString}. Ignoring`,\n );\n }\n continue;\n }\n try {\n let sliceStart = 0;\n if (value.startsWith(\"'\")) {\n sliceStart = 1;\n }\n let sliceEnd = value.length;\n if (value.endsWith(\"'\")) {\n sliceEnd -= 1;\n }\n\n const decoded = decodeURIComponent(value.slice(sliceStart, sliceEnd));\n // should we be trimming here?\n tags.push({ key: key.trim(), value: decoded });\n } catch (err) {\n // we want to be very conservative with this parser and ignore errors\n console.error(err);\n }\n }\n return { tags, queryWithoutTags };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ExportedStats } from "../optimizer/statistics.cjs";
|
|
4
|
+
import { RootIndexCandidate } from "../optimizer/genalgo.cjs";
|
|
5
|
+
import { Nudge } from "./nudges.cjs";
|
|
6
|
+
import { ColumnReferencePart } from "./walker.cjs";
|
|
7
|
+
import { NullTestType, SortByDir, SortByNulls } from "@pgsql/types";
|
|
8
|
+
|
|
9
|
+
//#region src/sql/analyzer.d.ts
|
|
10
|
+
interface DatabaseDriver {
|
|
11
|
+
query(query: string, params: unknown[]): Promise<unknown[]>;
|
|
12
|
+
}
|
|
13
|
+
declare const ignoredIdentifier = "__qd_placeholder";
|
|
14
|
+
interface SQLCommenterTag {
|
|
15
|
+
key: string;
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
18
|
+
type SortContext = {
|
|
19
|
+
dir: SortByDir;
|
|
20
|
+
nulls: SortByNulls;
|
|
21
|
+
};
|
|
22
|
+
type DiscoveredColumnReference = {
|
|
23
|
+
/** How often the column reference appears in the query. */frequency: number;
|
|
24
|
+
/**
|
|
25
|
+
* Representation of the column reference exactly
|
|
26
|
+
* as it appears in the query.
|
|
27
|
+
*/
|
|
28
|
+
representation: string;
|
|
29
|
+
/**
|
|
30
|
+
* Parts of the column reference separated by dots in the query.
|
|
31
|
+
* The table reference (if it exists) is resolved if the query
|
|
32
|
+
* uses an alias.
|
|
33
|
+
*
|
|
34
|
+
* Has 3 different potential configurations (in theory)
|
|
35
|
+
* `a.b.c` - a column reference with a table and a schema reference
|
|
36
|
+
* `a.b` - a column reference with a table reference but no schema
|
|
37
|
+
* `a` - a column reference with no table reference.
|
|
38
|
+
*
|
|
39
|
+
* We use a simple array here to allow parsing of any syntactically correct
|
|
40
|
+
* but logically incorrect query. The checks happen later when we're deriving
|
|
41
|
+
* potential indexes from parts of a column reference in `deriveIndexes`
|
|
42
|
+
*/
|
|
43
|
+
parts: ColumnReferencePart[];
|
|
44
|
+
/**
|
|
45
|
+
* Whether the column reference is invalid. This
|
|
46
|
+
*/
|
|
47
|
+
ignored: boolean; /** The position of the column reference in the query. */
|
|
48
|
+
position: {
|
|
49
|
+
start: number;
|
|
50
|
+
end: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A sort direction associated by the column reference.
|
|
54
|
+
* Only relevant to references from sorts
|
|
55
|
+
*/
|
|
56
|
+
sort?: SortContext;
|
|
57
|
+
where?: {
|
|
58
|
+
nulltest?: NullTestType;
|
|
59
|
+
};
|
|
60
|
+
jsonbOperator?: JsonbOperator;
|
|
61
|
+
jsonbExtraction?: string;
|
|
62
|
+
};
|
|
63
|
+
type JsonbOperator = "@>" | "?" | "?|" | "?&" | "@@" | "@?";
|
|
64
|
+
type StatementType = "select" | "insert" | "update" | "delete" | "other";
|
|
65
|
+
/** A function defined by @pgsql/parser */
|
|
66
|
+
type Parser = (query: string) => Promise<unknown>;
|
|
67
|
+
type TableReference = {
|
|
68
|
+
schema?: string;
|
|
69
|
+
table: string;
|
|
70
|
+
};
|
|
71
|
+
type AnalysisResult = {
|
|
72
|
+
statementType: StatementType;
|
|
73
|
+
indexesToCheck: DiscoveredColumnReference[];
|
|
74
|
+
ansiHighlightedQuery: string;
|
|
75
|
+
referencedTables: TableReference[];
|
|
76
|
+
shadowedAliases: ColumnReferencePart[];
|
|
77
|
+
tags: SQLCommenterTag[];
|
|
78
|
+
queryWithoutTags: string;
|
|
79
|
+
formattedQueryWithoutTags?: string;
|
|
80
|
+
nudges: Nudge[];
|
|
81
|
+
};
|
|
82
|
+
type SQLCommenterExtraction = {
|
|
83
|
+
tags: SQLCommenterTag[];
|
|
84
|
+
queryWithoutTags: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Analyzes a query and returns a list of column references that
|
|
88
|
+
* should be indexed.
|
|
89
|
+
*
|
|
90
|
+
* This should be instantiated once per analyzed query.
|
|
91
|
+
*/
|
|
92
|
+
declare class Analyzer {
|
|
93
|
+
private readonly parser;
|
|
94
|
+
constructor(parser: Parser);
|
|
95
|
+
analyze(query: string, formattedQuery?: string): Promise<AnalysisResult>;
|
|
96
|
+
deriveIndexes(tables: ExportedStats[], discovered: DiscoveredColumnReference[], referencedTables: TableReference[]): RootIndexCandidate[];
|
|
97
|
+
private filterReferences;
|
|
98
|
+
private hasColumn;
|
|
99
|
+
private colorizeKeywords;
|
|
100
|
+
/**
|
|
101
|
+
* Resolves aliases such as `a.b` to `x.b` if `a` is a known
|
|
102
|
+
* alias to a table called x.
|
|
103
|
+
*
|
|
104
|
+
* Ignores all other combination of parts such as `a.b.c`
|
|
105
|
+
*/
|
|
106
|
+
private resolveTableAliases;
|
|
107
|
+
private normalize;
|
|
108
|
+
private extractSqlcommenter;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { AnalysisResult, Analyzer, DatabaseDriver, DiscoveredColumnReference, JsonbOperator, Parser, SQLCommenterExtraction, SQLCommenterTag, SortContext, StatementType, TableReference, ignoredIdentifier };
|
|
112
|
+
//# sourceMappingURL=analyzer.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.cts","names":[],"sources":["../../src/sql/analyzer.ts"],"mappings":";;;;;;;;;UAmBiB,cAAA;EACf,KAAA,CAAM,KAAA,UAAe,MAAA,cAAoB,OAAA;AAAA;AAAA,cAG9B,iBAAA;AAAA,UAEI,eAAA;EACf,GAAA;EACA,KAAA;AAAA;AAAA,KAGU,WAAA;EACV,GAAA,EAAK,SAAA;EACL,KAAA,EAAO,WAAA;AAAA;AAAA,KAGG,yBAAA;EAZC,2DAcX,SAAA;;;;AAZF;EAiBE,cAAA;;;;AAZF;;;;;;;;;;AAKA;EAsBE,KAAA,EAAO,mBAAA;;;;EAIP,OAAA,WAYgB;EAVhB,QAAA;IACE,KAAA;IACA,GAAA;EAAA;EARF;;;;EAcA,IAAA,GAAO,WAAA;EACP,KAAA;IAAU,QAAA,GAAW,YAAA;EAAA;EACrB,aAAA,GAAgB,aAAA;EAChB,eAAA;AAAA;AAAA,KAGU,aAAA;AAAA,KAEA,aAAA;;KAUA,MAAA,IAAU,KAAA,aAAkB,OAAA;AAAA,KAE5B,cAAA;EACV,MAAA;EACA,KAAA;AAAA;AAAA,KAGU,cAAA;EACV,aAAA,EAAe,aAAA;EACf,cAAA,EAAgB,yBAAA;EAChB,oBAAA;EACA,gBAAA,EAAkB,cAAA;EAClB,eAAA,EAAiB,mBAAA;EACjB,IAAA,EAAM,eAAA;EACN,gBAAA;EACA,yBAAA;EACA,MAAA,EAAQ,KAAA;AAAA;AAAA,KAGE,sBAAA;EACV,IAAA,EAAM,eAAA;EACN,gBAAA;AAAA;;;AAdF;;;;cAuBa,QAAA;EAAA,iBACkB,MAAA;cAAA,MAAA,EAAQ,MAAA;EAC/B,OAAA,CACJ,KAAA,UACA,cAAA,YACC,OAAA,CAAQ,cAAA;EAuGX,aAAA,CACE,MAAA,EAAQ,aAAA,IACR,UAAA,EAAY,yBAAA,IACZ,gBAAA,EAAkB,cAAA,KACjB,kBAAA;EAAA,QAuHK,gBAAA;EAAA,QAmBA,SAAA;EAAA,QAMA,gBAAA;EAtRO;;;;;;EAAA,QA4SP,mBAAA;EAAA,QAkBA,SAAA;EAAA,QAOA,mBAAA;AAAA"}
|