@query-doctor/core 0.10.4 → 0.10.5

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-findings.mjs","names":[],"sources":["../../src/findings/query-findings.ts"],"sourcesContent":["import type { LiveQueryOptimization } from \"../query.js\";\nimport type { PostgresExplainStage } from \"../sql/database.js\";\nimport type { Nudge } from \"../sql/nudges.js\";\n\n/**\n * Plan-aware query findings.\n *\n * The *anti-pattern* nudges (parseNudges in ./sql/nudges.ts) are AST-only: they\n * read the SQL string and flag syntactic patterns, never the EXPLAIN plan — so\n * they can say \"you put a function on a column\" but not \"…and that's why this\n * table is being sequentially scanned.\" (The nudge array can also carry\n * plan-derived *improvement* nudges — LARGE/SMALL_IMPROVEMENT_FOUND — but those\n * just report the optimizer's index gain as a number, not a plan-shape diagnosis.)\n *\n * This module fills that gap. Each finder is a pure function over data Query\n * Doctor already captures — the stored EXPLAIN plan plus the nudge array — and\n * emits a computed, human-readable verdict (\"X is slow because Y\"), not another\n * raw property for the UI to render. The output is deterministic: same plan in,\n * same findings out.\n */\n\nexport type QueryFindingSeverity = \"critical\" | \"warning\" | \"info\";\n\nexport type QueryFindingCode =\n | \"COST_CONCENTRATION\"\n | \"REPEATED_INNER_LOOP\"\n | \"EXPENSIVE_SEQ_SCAN\"\n | \"SORT_WITHOUT_INDEX\"\n | \"WIDE_RESULT_NO_LIMIT\"\n | \"FUNCTION_ON_COLUMN_BLOCKS_INDEX\"\n | \"FULL_TABLE_SCAN\";\n\nexport interface QueryFinding {\n code: QueryFindingCode;\n severity: QueryFindingSeverity;\n /** One-line headline, safe to show as a list item title. */\n title: string;\n /** The \"this is wrong because Y\" sentence, in plain language. Only states what\n * the plan makes certain. */\n detail: string;\n /** An optional, explicitly-conditional next step — a *lead*, not a fix. Used\n * when there's a plausible improvement we can't be sure of (it depends on the\n * data), so it's hedged: \"often helps, verify before acting\". Prescriptions\n * that we ARE sure of stay in `detail`. */\n lead?: string;\n /**\n * Share of the query's cost this finding is worth fixing, 0–1. Mostly the\n * node's own cost share — but for a loop that's already its multiplied weight,\n * since Postgres folds the per-row repetition into the node's Total Cost; the\n * function-on-column finding scores by the *multiplied* cost of the scans it\n * blocks, and payload findings (not a cost share) get a size-derived score on\n * the same scale. Findings are returned sorted by it, so a reader attacks the\n * biggest lever first and isn't misled by a scary-but-tiny finding.\n */\n impact: number;\n /** Structured backing for the claim — rendered as evidence chips. */\n evidence?: Record<string, string | number>;\n}\n\n// Relevance floor. Findings are for queries worth optimising; below this total\n// cost the query is trivially fast even at the analyzer's predicted scale, so an\n// index or rewrite isn't worth a reader's attention — flagging it (e.g. \"an\n// index would help\" on a 6-row config-table scan) is just noise. Tunable; on the\n// dogfood payload it drops ~45% of finding-bearing queries, all genuinely fast.\nconst MIN_QUERY_COST = 100;\n// Access paths that already use an index. When one of these dominates the cost\n// and the optimizer found no better index, there's no lever — the cost is\n// inherent to the rows read, so \"target this node\" is empty advice, not a\n// finding. (A Seq Scan is excluded: it's the classic missing-index signal.)\nconst INDEX_SERVED_NODES = new Set([\n \"Index Only Scan\",\n \"Index Scan\",\n \"Bitmap Heap Scan\",\n \"Bitmap Index Scan\",\n]);\n// A single node contributing at least this share of the query's total cost is\n// worth calling out as where the time goes.\nconst COST_CONCENTRATION_SHARE = 0.5;\n// Above this share, the concentration is severe enough to warn rather than note.\nconst COST_CONCENTRATION_WARN_SHARE = 0.8;\n// A sequential scan is flagged when it carries this share of the cost on its own\n// or scans at least this many estimated rows — either makes it index-worthy.\nconst SEQ_SCAN_COST_SHARE = 0.3;\nconst SEQ_SCAN_ROWS = 50_000;\n// A node whose Total Cost dwarfs its children's single-pass cost by more than\n// this is re-executing a child per row — the no-EXPLAIN-ANALYZE tell of loop\n// multiplication. Pair it with a minimum share so only loops that actually\n// matter are flagged, and cap how many we report per query to avoid noise.\nconst LOOP_RATIO = 10;\nconst LOOP_SHARE = 0.25;\nconst MAX_LOOP_FINDINGS = 3;\n// A Sort node carrying at least this share of the query's cost is worth flagging\n// as removable with a matching index, rather than paid on every call.\nconst SORT_COST_SHARE = 0.2;\nconst SORT_WARN_SHARE = 0.4;\n// Estimated result payload (rows × width) that makes an unbounded query notable\n// now, and the point at which it warrants a warning rather than a note.\nconst WIDE_RESULT_BYTES = 100_000;\nconst WIDE_RESULT_WARN_BYTES = 256_000;\n// A node's cost is split into named components (disk I/O, per-row CPU, …). We\n// only name the dominant one when it carries at least this share of the node's\n// own cost — below it, no single component explains where the cost goes.\nconst COST_DRIVER_SHARE = 0.4;\n// A scan that reads many rows but returns very few is discarding nearly all its\n// work — the strongest missing-index signal. Being a ratio (output ÷ scanned) it\n// survives the analyzer's synthetic scaling: a table faked to 10M rows still\n// returns the same handful. Only stated when the scan is sizeable and the filter\n// genuinely selective, so it never fires on a small table or a broad scan.\nconst SELECTIVE_SCAN_MIN_SCANNED = 10_000;\nconst SELECTIVE_SCAN_MAX_RATIO = 0.05;\n\n/**\n * The plan is typed as a discriminated union whose variants only cover a handful\n * of node types; every other field (Plan Rows, Startup Cost, Sort/Nested Loop\n * nodes …) arrives via the schema's `.passthrough()`. So we read it as a loose\n * record and pull values defensively rather than fighting the union.\n */\ntype PlanNode = Record<string, unknown> & { Plans?: PlanNode[] };\n\nfunction asNode(plan: PostgresExplainStage): PlanNode {\n return plan as unknown as PlanNode;\n}\n\nfunction num(node: PlanNode, key: string): number | undefined {\n const value = node[key];\n return typeof value === \"number\" ? value : undefined;\n}\n\nfunction str(node: PlanNode, key: string): string | undefined {\n const value = node[key];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction childNodes(node: PlanNode): PlanNode[] {\n return Array.isArray(node.Plans) ? node.Plans : [];\n}\n\nfunction walk(node: PlanNode, visit: (node: PlanNode) => void): void {\n visit(node);\n for (const child of childNodes(node)) walk(child, visit);\n}\n\n/**\n * Postgres reports cumulative Total Cost (a node's cost includes its children).\n * A node's own contribution is therefore its Total Cost minus its children's —\n * which telescopes so the self-costs across the tree sum back to the root total.\n */\nfunction selfCost(node: PlanNode): number {\n const total = num(node, \"Total Cost\") ?? 0;\n const childTotal = childNodes(node).reduce(\n (sum, child) => sum + (num(child, \"Total Cost\") ?? 0),\n 0,\n );\n return Math.max(0, total - childTotal);\n}\n\nfunction nodeLabel(node: PlanNode): string {\n const type = str(node, \"Node Type\") ?? \"node\";\n const relation = str(node, \"Relation Name\") ?? str(node, \"Alias\");\n return relation ? `${type} on ${relation}` : type;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;\n if (bytes >= 1_000) return `${Math.round(bytes / 1_000)} KB`;\n return `${Math.round(bytes)} B`;\n}\n\n/**\n * QD's augmented planner annotates each plan node with a `Cost Breakdown`: the\n * node's cost split into named components ({ Cost, Reason, Formula?, Variables? }),\n * each a real planner term — sequential disk I/O over relpages, per-row heap\n * filtering over reltuples, random index-page access, and so on. Vanilla Postgres\n * plans don't carry it, so it's absent for un-augmented optimizations.\n */\ninterface CostComponent {\n cost: number;\n reason: string;\n variables: Record<string, unknown>;\n}\n\nfunction costBreakdown(node: PlanNode): CostComponent[] {\n const raw = node[\"Cost Breakdown\"];\n if (!Array.isArray(raw)) return [];\n const components: CostComponent[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const cost = record[\"Cost\"];\n const reason = record[\"Reason\"];\n if (typeof cost !== \"number\" || typeof reason !== \"string\") continue;\n const variables =\n typeof record[\"Variables\"] === \"object\" && record[\"Variables\"] !== null\n ? (record[\"Variables\"] as Record<string, unknown>)\n : {};\n components.push({ cost, reason, variables });\n }\n return components;\n}\n\n/** Plain-language name for a cost component, citing its load-bearing cardinality\n * (the planner's assumed relpages / reltuples). Maps only the Reasons QD's\n * planner actually emits; an unmapped one falls back to a generic phrase so a new\n * component never breaks the sentence. The distinction that matters to a reader\n * is disk I/O (the table or index is big on disk — fewer pages or a covering\n * index helps) vs per-row CPU (many rows flow through — better selectivity helps). */\nfunction costComponentPhrase(component: CostComponent): string {\n const cardinality = (key: string): string | undefined => {\n const value = component.variables[key];\n return typeof value === \"number\"\n ? value.toLocaleString(\"en-US\")\n : undefined;\n };\n switch (component.reason) {\n case \"RUNTIME:DISK_IO\": {\n const pages = cardinality(\"relpages\");\n return pages\n ? `reading the table off disk (~${pages} pages)`\n : \"sequential disk reads\";\n }\n case \"RUNTIME:DISK_ACCESS\": {\n const pages = cardinality(\"index_pages_fetched\");\n return pages\n ? `random index-page reads (~${pages} pages)`\n : \"random index-page reads\";\n }\n case \"RUNTIME:WORST_CASE_IO\": {\n const pages = cardinality(\"pages_fetched\");\n return pages\n ? `worst-case random heap reads (~${pages} pages, assuming low page visibility)`\n : \"worst-case random heap reads\";\n }\n case \"RUNTIME:HEAP_FETCH_AND_FILTER\": {\n const rows = cardinality(\"reltuples\");\n return rows\n ? `fetching and filtering ~${rows} rows from the heap`\n : \"per-row heap fetch and filter\";\n }\n case \"RUNTIME:HEAP_FILTER\": {\n const rows = cardinality(\"reltuples\");\n return rows ? `filtering ~${rows} rows` : \"per-row filtering\";\n }\n case \"RUNTIME:INDEX_FILTER\":\n return \"scanning index entries\";\n case \"RUNTIME:BITMAP\":\n return \"building the row bitmap\";\n default:\n return \"its main cost component\";\n }\n}\n\n/** A one-sentence \"where this node's cost goes\", derived from its Cost Breakdown:\n * the single largest positive component, in plain language. Discounts (negative\n * parallel-worker / I/O-correlation adjustments) and ~zero startup terms aren't\n * where the cost lands, so they're dropped. Returns \"\" when the node has no\n * breakdown (vanilla plan) or no component clearly dominates — callers append it\n * unconditionally and get nothing when there's nothing certain to add. */\nfunction whyExpensive(node: PlanNode): string {\n const positive = costBreakdown(node).filter((c) => c.cost > 0);\n if (positive.length === 0) return \"\";\n const total = positive.reduce((sum, c) => sum + c.cost, 0);\n const dominant = positive.reduce((a, b) => (b.cost > a.cost ? b : a));\n if (total <= 0 || dominant.cost / total < COST_DRIVER_SHARE) return \"\";\n return ` Most of that node's cost is ${costComponentPhrase(dominant)}.`;\n}\n\n/** Rows a scan node reads, from its Cost Breakdown's `reltuples` (the planner's\n * table-size estimate) — distinct from Plan Rows, which is the *post-filter*\n * output. Undefined when the node carries no breakdown cardinality (vanilla plan). */\nfunction rowsScanned(node: PlanNode): number | undefined {\n let max: number | undefined;\n for (const component of costBreakdown(node)) {\n const value = component.variables[\"reltuples\"];\n if (typeof value === \"number\" && (max === undefined || value > max)) {\n max = value;\n }\n }\n return max;\n}\n\n/** A diagnosis sentence for a scan that reads far more rows than it returns: the\n * filter throws away nearly everything it touched, which is exactly the work an\n * index avoids. Pure diagnosis — it states the waste, not a fix; the optimizer's\n * index verdict (handled by the caller) decides whether an index is the answer.\n * \"\" when the node isn't a selective scan or has no breakdown to measure it from. */\nfunction selectivityClause(node: PlanNode): string {\n const scanned = rowsScanned(node);\n const returned = num(node, \"Plan Rows\");\n if (scanned === undefined || returned === undefined) return \"\";\n if (scanned < SELECTIVE_SCAN_MIN_SCANNED) return \"\";\n const ratio = scanned > 0 ? returned / scanned : 1;\n if (ratio >= SELECTIVE_SCAN_MAX_RATIO) return \"\";\n const pct = ratio < 0.0001 ? \"<0.01%\" : `~${(ratio * 100).toFixed(2)}%`;\n return ` It scans ~${scanned.toLocaleString(\n \"en-US\",\n )} rows and returns ~${returned.toLocaleString(\n \"en-US\",\n )} (${pct}), so nearly all of that work is rows the filter discards.`;\n}\n\n/** A cost as a fraction of the query total, clamped to [0,1]. Parallel plans\n * (Gather / Gather Merge) report a child node's cost as the per-worker figure —\n * which can exceed the gathered root total, since the Gather divides the work.\n * Left unclamped that yields a nonsensical >100% share. */\nfunction clampShare(cost: number, total: number): number {\n if (total <= 0) return 0;\n return Math.min(1, cost / total);\n}\n\n/** A node that reads an entire relation with nothing to narrow it: a Seq Scan or\n * full Index Only Scan carrying no Filter / Index Cond / Recheck Cond. The cost\n * is the whole table by definition — and since there's no predicate, no index can\n * avoid the pass (which is why the optimizer returns no_improvement_found). */\nfunction isUnfilteredFullRead(node: PlanNode): boolean {\n const type = str(node, \"Node Type\");\n if (type !== \"Seq Scan\" && type !== \"Index Only Scan\") return false;\n return (\n node[\"Filter\"] == null &&\n node[\"Index Cond\"] == null &&\n node[\"Recheck Cond\"] == null\n );\n}\n\n/** Whether the plan aggregates, and how — a scalar aggregate (count(*) over the\n * whole table, no grouping) can be approximated or maintained out of band, a\n * grouped one (GROUP BY / HAVING) genuinely needs every row, and neither rules\n * the full read. Grouped wins when both appear (e.g. `count(*)` over a grouped\n * subquery), since the grouping is what forces the whole-table pass. */\nfunction aggregateShape(root: PlanNode): \"scalar\" | \"grouped\" | \"none\" {\n let scalar = false;\n let grouped = false;\n walk(root, (node) => {\n if (str(node, \"Node Type\") !== \"Aggregate\") return;\n const groupKey = node[\"Group Key\"];\n if (Array.isArray(groupKey) && groupKey.length > 0) grouped = true;\n else scalar = true;\n });\n return grouped ? \"grouped\" : scalar ? \"scalar\" : \"none\";\n}\n\n/** FULL_TABLE_SCAN: an unfiltered whole-relation read that dominates the cost\n * while the optimizer found no index that helps — because there's no predicate to\n * index. Without this the query shows *zero* findings (the dominant node is\n * index-served, so the concentration pass suppresses it), which reads as \"QD\n * didn't look\". This fills that silence with the reason and an honest lead. */\nfunction buildFullTableReadFinding(\n node: PlanNode,\n share: number,\n shape: \"scalar\" | \"grouped\" | \"none\",\n): QueryFinding {\n const pct = Math.round(share * 100);\n const relation = relationOf(node);\n const target = relation ? `\\`${relation}\\`` : \"The table\";\n let detail: string;\n let lead: string | undefined;\n if (shape === \"scalar\") {\n detail = `${target} is read end to end to compute the aggregate — there's no filter to narrow it, so no index avoids the full pass (the optimizer confirmed none helps).`;\n lead = `If this runs often, an approximate count (\\`pg_class.reltuples\\`) or a maintained tally avoids re-reading the whole table each call.`;\n } else if (shape === \"grouped\") {\n detail = `${target} is read end to end to group every row — there's no filter to narrow it, so the whole-table pass is inherent to the query.`;\n } else {\n detail = `${target} is read end to end — there's no filter to narrow it, so no index avoids the full pass (the optimizer confirmed none helps).`;\n lead = `If you don't need every row, a WHERE filter or LIMIT lets an index read only part of the table.`;\n }\n return {\n code: \"FULL_TABLE_SCAN\",\n severity: \"info\",\n impact: share,\n title: relation ? `Whole-table read of ${relation}` : \"Whole-table read\",\n detail: `${detail}${whyExpensive(node)}`,\n ...(lead ? { lead } : {}),\n evidence: {\n ...(relation ? { relation } : {}),\n costShare: `${pct}%`,\n },\n };\n}\n\n/** Sort nodes a LIMIT bounds directly (top-N) — `Limit → Sort`, possibly through\n * pass-through nodes (Result / Gather Merge / Gather). For these the win from a\n * matching index is bigger than skipping the sort: the LIMIT also lets the scan\n * stop after the first n rows instead of sorting the whole set. A Limit above an\n * Aggregate or a Join doesn't bound the sort below it, so the descent stops at\n * any non-pass-through node. */\nconst LIMIT_PASSTHROUGH = new Set([\n \"Result\",\n \"Gather\",\n \"Gather Merge\",\n \"LockRows\",\n]);\nfunction sortsBoundedByLimit(root: PlanNode): Set<PlanNode> {\n const bounded = new Set<PlanNode>();\n walk(root, (node) => {\n if (str(node, \"Node Type\") !== \"Limit\") return;\n let cursor: PlanNode | undefined = node;\n const seen = new Set<PlanNode>();\n while (cursor && !seen.has(cursor)) {\n seen.add(cursor);\n const child = childNodes(cursor)[0];\n if (!child) break;\n const type = str(child, \"Node Type\");\n if (type === \"Sort\") {\n bounded.add(child);\n break;\n }\n if (!LIMIT_PASSTHROUGH.has(type ?? \"\")) break;\n cursor = child;\n }\n });\n return bounded;\n}\n\n/** Advice for a dominant Sort node. Seq Scan index advice keys off the\n * optimizer's verdict (handled inline); a Nested Loop is a multiplication (its\n * own finding); everything else has no generic hint — hence Sort-only here. */\nfunction sortHint(nodeType: string | undefined): string {\n return nodeType === \"Sort\"\n ? \" It's an in-memory sort — an index matching the ORDER BY could avoid it.\"\n : \"\";\n}\n\nfunction relationOf(node: PlanNode): string | undefined {\n return str(node, \"Relation Name\") ?? str(node, \"Alias\");\n}\n\n/** A nested loop's two inputs: the outer side (driven once, its row count is the\n * loop count) and the inner side (re-run once per outer row). Postgres tags them\n * via Parent Relationship; fall back to child order when the tag is absent. */\nfunction loopSides(node: PlanNode): { outer?: PlanNode; inner?: PlanNode } {\n const kids = childNodes(node);\n const outer =\n kids.find((k) => str(k, \"Parent Relationship\") === \"Outer\") ?? kids[0];\n const inner =\n kids.find((k) => str(k, \"Parent Relationship\") === \"Inner\") ?? kids[1];\n return { outer, inner };\n}\n\n// Honest, conditional lead (not a prescription): per-row correlated subqueries\n// can often be restructured set-based, but whether it's faster depends on the\n// data, so it's explicitly hedged. Names the subquery's table when we know it.\nfunction setBasedLead(relation?: string): string {\n const target = relation ? `\\`${relation}\\`` : \"the subquery's table\";\n return `This subquery might fold into one set-based pass: aggregate or join ${target} once, then filter. Often much faster, but it depends on the data, so check before committing.`;\n}\n\n/** A node's SubPlan children — correlated subqueries (EXISTS, scalar) run once\n * per row, whether in the Filter (EXISTS) or the SELECT list (a scalar COUNT).\n * Parent Relationship \"SubPlan\" is the reliable tell; an InitPlan (also carries\n * a Subplan Name) runs once, so it's deliberately excluded. */\nfunction subplanChildren(node: PlanNode): PlanNode[] {\n return childNodes(node).filter(isSubplanChild);\n}\n\n/** REPEATED_INNER_LOOP for the SubPlan-on-a-node shape: a Seq/Index Scan (or\n * other node) that evaluates correlated subqueries once per row. The planner\n * folds that multiplied cost into the node's own total, so a naive reading\n * blames the scan and says \"add an index\" — but the cost is the per-row\n * repetition, not the scan. Covers both the d2armory count(*) (EXISTS subplans\n * in the Filter) and the migration backfill (scalar subplans in the SELECT). */\nfunction buildSubplanFinding(\n node: PlanNode,\n self: number,\n rootTotal: number,\n): QueryFinding {\n const share = clampShare(self, rootTotal);\n const pct = Math.round(share * 100);\n const relation = relationOf(node);\n const nodeType = str(node, \"Node Type\") ?? \"scan\";\n const subplans = subplanChildren(node);\n const loops = num(node, \"Plan Rows\");\n const loopsText =\n loops !== undefined ? ` (~${loops.toLocaleString(\"en-US\")} rows)` : \"\";\n const count = subplans.length;\n const noun =\n count === 1 ? \"a correlated subquery\" : `${count} correlated subqueries`;\n const indexClause = relation\n ? ` An index on \\`${relation}\\` can shrink the scan but not the per-row subqueries.`\n : ``;\n // The subquery's own driving table — the thing a set-based rewrite aggregates.\n let subqueryRelation: string | undefined;\n for (const subplan of subplans) {\n const driver = heaviestDriver(subplan);\n const rel = driver ? relationOf(driver) : undefined;\n if (rel) {\n subqueryRelation = rel;\n break;\n }\n }\n return {\n code: \"REPEATED_INNER_LOOP\",\n severity: share >= COST_CONCENTRATION_WARN_SHARE ? \"warning\" : \"info\",\n impact: share,\n title: \"Correlated subquery runs once per row\",\n detail: `This ${nodeType}${\n relation ? ` over \\`${relation}\\`` : \"\"\n } runs ${noun} once per row${loopsText}, so about ${pct}% of the cost is that repetition, not the scan.${indexClause}`,\n lead: setBasedLead(subqueryRelation),\n evidence: {\n ...(relation ? { relation } : {}),\n ...(loops !== undefined ? { loops } : {}),\n subqueries: count,\n costShare: `${pct}%`,\n },\n };\n}\n\n/** The inner side is a correlated subquery (re-evaluated per outer row) rather\n * than a plain table probe — the tell is a Memoize/Subquery/SubPlan in its\n * subtree. Postgres only inserts Memoize to cache a repeatedly-run inner, so its\n * presence is itself the signal that work is being multiplied per row. */\nfunction innerIsCorrelatedSubquery(inner: PlanNode): boolean {\n let found = false;\n walk(inner, (n) => {\n const type = str(n, \"Node Type\");\n if (type === \"Memoize\" || type === \"Subquery Scan\") found = true;\n if (str(n, \"Parent Relationship\") === \"SubPlan\") found = true;\n if (typeof n[\"Subplan Name\"] === \"string\") found = true;\n });\n return found;\n}\n\n/** The single heaviest node within a subtree (by own cost), used to name what\n * actually drives an inner side's per-loop cost. */\nfunction heaviestDriver(subtree: PlanNode): PlanNode | undefined {\n let best: PlanNode | undefined;\n let bestSelf = -1;\n walk(subtree, (n) => {\n const s = selfCost(n);\n if (s > bestSelf) {\n bestSelf = s;\n best = n;\n }\n });\n return best;\n}\n\n/** True when a child node is a correlated SubPlan (e.g. a scalar COUNT subquery\n * in the SELECT list), which re-runs per row of its parent rather than once. */\nfunction isSubplanChild(node: PlanNode): boolean {\n return str(node, \"Parent Relationship\") === \"SubPlan\";\n}\n\n/**\n * The node whose *multiplied* cost (own cost × how many times it runs) is\n * largest within a loop's per-row work — i.e. where the repeated cost truly\n * lands. A node cheap per run (a 631-cost heap fetch, an 8-cost COUNT) can\n * dominate once run thousands of times; a plain \"heaviest node\" reading misses\n * it. Per-row work hangs off the loop two ways: the inner side (run per outer\n * row) and correlated SubPlan children (run per *output* row, e.g. scalar\n * subqueries in the SELECT). Both are walked. No-EXPLAIN-ANALYZE: executions\n * come from Plan Rows × nested-loop / subplan multipliers, never real loops.\n */\nfunction multipliedDriver(\n loopNode: PlanNode,\n baseLoops: number,\n): { node: PlanNode; multiplied: number } | undefined {\n const { inner } = loopSides(loopNode);\n const subplans = subplanChildren(loopNode);\n const outputRows = num(loopNode, \"Plan Rows\") ?? baseLoops;\n\n let best: PlanNode | undefined;\n let bestCost = -1;\n const visit = (node: PlanNode, executions: number) => {\n const cost = selfCost(node) * executions;\n if (cost > bestCost) {\n bestCost = cost;\n best = node;\n }\n let innerChild: PlanNode | undefined;\n let outerRows = 1;\n if (str(node, \"Node Type\") === \"Nested Loop\") {\n const sides = loopSides(node);\n innerChild = sides.inner;\n outerRows = sides.outer ? (num(sides.outer, \"Plan Rows\") ?? 1) : 1;\n }\n const rows = num(node, \"Plan Rows\") ?? 1;\n for (const child of childNodes(node)) {\n const childExecutions =\n child === innerChild\n ? executions * outerRows\n : isSubplanChild(child)\n ? executions * rows\n : executions;\n visit(child, childExecutions);\n }\n };\n if (inner) visit(inner, baseLoops);\n for (const subplan of subplans) visit(subplan, outputRows);\n return best ? { node: best, multiplied: bestCost } : undefined;\n}\n\n/** Ratio of a node's Total Cost to its children's combined single-pass cost.\n * >LOOP_RATIO means the node re-runs a child per row (loop multiplication). */\nfunction loopRatio(node: PlanNode): number {\n const childTotal = childNodes(node).reduce(\n (sum, child) => sum + (num(child, \"Total Cost\") ?? 0),\n 0,\n );\n const total = num(node, \"Total Cost\") ?? 0;\n return childTotal > 0 ? total / childTotal : 0;\n}\n\n/** Identity of what a nested loop repeats, so twin loops across subplans (same\n * correlated inner) collapse to one finding instead of several. */\nfunction loopSignature(node: PlanNode): string {\n const { inner } = loopSides(node);\n const driver = inner ? heaviestDriver(inner) : undefined;\n const driverRel = driver ? relationOf(driver) : undefined;\n const correlated = inner ? innerIsCorrelatedSubquery(inner) : false;\n return `${driverRel ?? \"?\"}|${correlated}`;\n}\n\n/** Build the REPEATED_INNER_LOOP verdict for a nested loop. Diagnose, don't\n * prescribe: decompose the cost into loops × per-loop, name what drives the\n * per-loop cost, and — when the inner is a correlated subquery — state the\n * load-bearing fact that an index can't remove the per-row repetition. No\n * rewrite advice: whether a rewrite helps is conditional and a human's call. */\nfunction buildLoopFinding(\n node: PlanNode,\n self: number,\n rootTotal: number,\n): QueryFinding {\n const share = clampShare(self, rootTotal);\n const pct = Math.round(share * 100);\n const { outer, inner } = loopSides(node);\n const loops = outer ? num(outer, \"Plan Rows\") : undefined;\n const loopsText =\n loops !== undefined ? `~${loops.toLocaleString(\"en-US\")}` : \"many\";\n // Correlated either way: the inner side is a subquery, OR the loop carries\n // correlated SubPlan children (scalar subqueries in the SELECT that re-run\n // per row) — the case the GET /projects list hit.\n const correlated =\n (inner ? innerIsCorrelatedSubquery(inner) : false) ||\n childNodes(node).some(isSubplanChild);\n\n // Where the repeated cost truly lands — the node that's cheap per run but\n // dominant once multiplied by the loop count. This is the actionable target;\n // the loop itself is just the accumulator.\n const driver =\n loops !== undefined ? multipliedDriver(node, loops) : undefined;\n const driverNode = driver?.node;\n const driverRel = driverNode ? relationOf(driverNode) : undefined;\n const driverType = driverNode ? str(driverNode, \"Node Type\") : undefined;\n const driverShare = driver\n ? clampShare(driver.multiplied, rootTotal)\n : undefined;\n const driverPct =\n driverShare !== undefined ? Math.round(driverShare * 100) : undefined;\n\n const driverText =\n driverRel && driverType && driverPct !== undefined\n ? ` Most of it is the ${driverType} on \\`${driverRel}\\`, run ${loopsText} times (about ${driverPct}% of the query).`\n : ``;\n // The repetition is structural for a correlated subquery: it re-runs per row\n // no matter the access path. Whether the driver above is even index-improvable\n // is node-specific (a heap fetch of wide rows isn't), so we don't claim it —\n // we state only the load-bearing, unconditional fact.\n const structural = correlated\n ? ` It's a correlated subquery, so an index can speed each run but won't cut the repeats.`\n : ``;\n\n return {\n code: \"REPEATED_INNER_LOOP\",\n severity: share >= COST_CONCENTRATION_WARN_SHARE ? \"warning\" : \"info\",\n impact: share,\n title: correlated\n ? \"Correlated subquery runs once per row\"\n : \"Inner side re-runs once per row\",\n detail: `A nested loop re-runs its inner side once per row (${loopsText} rows), so about ${pct}% of the cost is that repetition, not one node.${driverText}${structural}`,\n ...(correlated ? { lead: setBasedLead(driverRel) } : {}),\n evidence: {\n ...(loops !== undefined ? { loops } : {}),\n ...(correlated ? { innerKind: \"correlated subquery\" } : {}),\n ...(driverRel ? { driver: driverRel } : {}),\n ...(driverPct !== undefined ? { driverShare: `${driverPct}%` } : {}),\n costShare: `${pct}%`,\n },\n };\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// Plan node fields that carry a condition expression as text.\nconst CONDITION_KEYS = [\n \"Hash Cond\",\n \"Join Filter\",\n \"Filter\",\n \"Recheck Cond\",\n \"Merge Cond\",\n \"Index Cond\",\n] as const;\n\n/**\n * Relations whose columns are wrapped in a function (or cast) inside some plan\n * condition — i.e. the relations a function-on-column actually blocks an index\n * on. The function-on-column nudge is AST-only and doesn't know which table is\n * affected; reading it off the plan's condition strings (e.g. `lower(cfg.repo)`,\n * mapping the alias back to its relation) is what stops us from blaming an\n * unrelated table that just happens to be sequentially scanned.\n */\nfunction relationsWrappedInFunction(root: PlanNode): Set<string> {\n const conditions: string[] = [];\n const aliasToRelation = new Map<string, string>();\n walk(root, (node) => {\n for (const key of CONDITION_KEYS) {\n const value = node[key];\n if (typeof value === \"string\") conditions.push(value);\n }\n const relation = str(node, \"Relation Name\");\n if (relation) {\n aliasToRelation.set(relation, relation);\n const alias = str(node, \"Alias\");\n if (alias) aliasToRelation.set(alias, relation);\n }\n });\n\n const wrapped = new Set<string>();\n for (const [aliasOrName, relation] of aliasToRelation) {\n const ref = escapeRegExp(aliasOrName);\n // A function call whose argument list (no nested parens) contains `ref.col`,\n // or a cast of `ref.col` — both forms the planner prints for the pattern.\n const inFunction = new RegExp(`[a-z_][a-z0-9_]*\\\\([^()]*\\\\b${ref}\\\\.`, \"i\");\n const inCast = new RegExp(`\\\\b${ref}\\\\.[a-z_][a-z0-9_]*\\\\s*::`, \"i\");\n if (conditions.some((c) => inFunction.test(c) || inCast.test(c))) {\n wrapped.add(relation);\n }\n }\n return wrapped;\n}\n\nfunction planOf(\n optimization: LiveQueryOptimization | undefined,\n): PostgresExplainStage | undefined {\n if (!optimization) return undefined;\n if (\n optimization.state === \"improvements_available\" ||\n optimization.state === \"no_improvement_found\"\n ) {\n return optimization.explainPlan;\n }\n return undefined;\n}\n\n/**\n * Compute the plan-aware findings for one query from its stored optimization and\n * nudges. Returns [] when there's no plan to reason over (waiting, optimizing,\n * not_supported, timeout, error) — the syntactic nudges still stand on their own.\n */\nexport function analyzeQueryFindings(\n optimization: LiveQueryOptimization | undefined,\n nudges: Nudge[] = [],\n): QueryFinding[] {\n const plan = planOf(optimization);\n if (!plan) return [];\n\n const root = asNode(plan);\n const rootTotal = num(root, \"Total Cost\") ?? 0;\n // Skip queries too cheap to be worth optimising — see MIN_QUERY_COST.\n if (rootTotal < MIN_QUERY_COST) return [];\n\n // The optimizer's own index verdict — the one thing we're *certain* of, since\n // the recommender already ran the planner with and without candidate indexes.\n // A plan only exists for improvements_available / no_improvement_found, so an\n // index-related finding is always one of these two. Prefer naming the proven\n // index; never tell an agent to add one the optimizer already searched for and\n // didn't find.\n const recommendedDefs =\n optimization?.state === \"improvements_available\"\n ? optimization.indexRecommendations\n .map((rec) => rec.definition)\n .filter((def): def is string => typeof def === \"string\" && def !== \"\")\n : [];\n const recommendedClause =\n recommendedDefs.length > 0\n ? ` The recommended index ${recommendedDefs\n .map((def) => `\\`${def}\\``)\n .join(\" / \")} would help this.`\n : \"\";\n // Index advice for a scan finding: name the proven index, or — when the\n // optimizer searched and found none — say so factually (it doesn't mean *no*\n // index can ever help; the candidate set is limited), rather than the generic\n // \"add an index\" that would contradict the optimizer.\n const scanIndexClause =\n recommendedClause ||\n (optimization?.state === \"no_improvement_found\"\n ? \" The optimizer checked for an index and found none that helps here.\"\n : \" An index on the filtered or joined columns would likely remove it.\");\n\n const nodes: { node: PlanNode; self: number }[] = [];\n walk(root, (node) => nodes.push({ node, self: selfCost(node) }));\n\n // How many times each node runs (no EXPLAIN ANALYZE): 1, multiplied by every\n // ancestor nested loop's outer-row count on its inner side. Lets findings rank\n // by *multiplied* cost — a cheap node looped thousands of times outweighs an\n // expensive one run once.\n const executions = new Map<PlanNode, number>();\n const countExecutions = (node: PlanNode, runs: number) => {\n executions.set(node, runs);\n let innerChild: PlanNode | undefined;\n let outerRows = 1;\n if (str(node, \"Node Type\") === \"Nested Loop\") {\n const sides = loopSides(node);\n innerChild = sides.inner;\n outerRows = sides.outer ? (num(sides.outer, \"Plan Rows\") ?? 1) : 1;\n }\n for (const child of childNodes(node)) {\n countExecutions(child, child === innerChild ? runs * outerRows : runs);\n }\n };\n countExecutions(root, 1);\n const multipliedCost = (node: PlanNode) =>\n selfCost(node) * (executions.get(node) ?? 1);\n\n const findings: QueryFinding[] = [];\n\n // Nodes already explained by a finding below, so the seq-scan / sort passes\n // don't report the same node twice.\n const reported = new Set<PlanNode>();\n\n // 1a. Loop multiplication, anywhere in the tree. A nested loop's cost is\n // outer_rows × inner_cost, not work the node does — so report it as that\n // decomposition. A node is a genuine multiplier when its cost dwarfs its\n // children's single-pass cost (ratio > LOOP_RATIO): the no-EXPLAIN-ANALYZE\n // tell that the inner re-runs per outer row. Walking the whole tree (not just\n // the single worst node) catches an inner that's cheap alone but expensive\n // because it loops — the case a \"where's the cost\" headline misses.\n if (rootTotal > 0) {\n const loopNodes = nodes\n .filter(\n ({ node, self }) =>\n str(node, \"Node Type\") === \"Nested Loop\" &&\n clampShare(self, rootTotal) >= LOOP_SHARE &&\n loopRatio(node) > LOOP_RATIO,\n )\n .sort((a, b) => b.self - a.self);\n const seenSignatures = new Set<string>();\n for (const { node, self } of loopNodes) {\n if (reported.size >= MAX_LOOP_FINDINGS) break;\n const sig = loopSignature(node);\n if (seenSignatures.has(sig)) continue;\n seenSignatures.add(sig);\n findings.push(buildLoopFinding(node, self, rootTotal));\n reported.add(node);\n }\n }\n\n // 1b. The single most expensive node, when it's a concentration we haven't\n // already covered as a loop. A leaf (scan/sort/…) gets a plain concentration\n // note; a dominant nested loop missed above (its cost is in its children, not\n // multiplied) still gets the loop decomposition. Skipped for trivial\n // single-node plans, where \"100% is one node\" says nothing.\n if (rootTotal > 0 && nodes.length > 1) {\n const top = nodes.reduce((best, candidate) =>\n candidate.self > best.self ? candidate : best,\n );\n const share = clampShare(top.self, rootTotal);\n if (share >= COST_CONCENTRATION_SHARE && !reported.has(top.node)) {\n const nodeType = str(top.node, \"Node Type\");\n if (nodeType === \"Nested Loop\") {\n findings.push(buildLoopFinding(top.node, top.self, rootTotal));\n } else if (subplanChildren(top.node).length > 0) {\n // A node whose cost is correlated subqueries run per row — in the Filter\n // (EXISTS) or the SELECT (scalar). The cost is the repetition, not the\n // scan, so diagnose it as such instead of saying \"add an index\".\n findings.push(buildSubplanFinding(top.node, top.self, rootTotal));\n } else if (\n INDEX_SERVED_NODES.has(nodeType ?? \"\") &&\n optimization?.state === \"no_improvement_found\"\n ) {\n // Already index-served and the optimizer found no better index — no\n // lever. But if it's an unfiltered whole-table read, the *reason* there's\n // no lever (no predicate to narrow) is worth stating, so the query isn't\n // left silent despite a real cost. Otherwise stay quiet (filtered scan,\n // genuinely no better index).\n if (isUnfilteredFullRead(top.node)) {\n findings.push(\n buildFullTableReadFinding(top.node, share, aggregateShape(root)),\n );\n }\n } else {\n const pct = Math.round(share * 100);\n findings.push({\n code: \"COST_CONCENTRATION\",\n severity: share >= COST_CONCENTRATION_WARN_SHARE ? \"warning\" : \"info\",\n impact: share,\n title: `${pct}% of the cost is one node`,\n detail: `About ${pct}% of this query's estimated cost is a single ${nodeLabel(\n top.node,\n )}. ${\n share >= COST_CONCENTRATION_WARN_SHARE\n ? \"It's the one thing worth tuning here.\"\n : \"It's the biggest single contributor.\"\n }${whyExpensive(top.node)}${\n nodeType === \"Seq Scan\"\n ? selectivityClause(top.node) + scanIndexClause\n : sortHint(nodeType) + recommendedClause\n }`,\n evidence: {\n node: nodeLabel(top.node),\n costShare: `${pct}%`,\n nodeCost: Math.round(top.self),\n totalCost: Math.round(rootTotal),\n },\n });\n }\n reported.add(top.node);\n }\n }\n\n // 2. Sequential scans that are expensive in their own right — by cost share or\n // by raw rows scanned. A query can scan the same table in several subplans, so\n // aggregate by relation: report each scanned table once, summing its cost share\n // across nodes rather than emitting a near-identical finding per node. The\n // dominant-node finding already names the single worst node, so skip that one.\n const seqScansByRelation = new Map<\n string,\n {\n self: number;\n rows: number | undefined;\n topNode: PlanNode;\n topSelf: number;\n }\n >();\n for (const { node, self } of nodes) {\n if (str(node, \"Node Type\") !== \"Seq Scan\") continue;\n if (reported.has(node)) continue;\n const relation = str(node, \"Relation Name\") ?? str(node, \"Alias\") ?? \"?\";\n const rows = num(node, \"Plan Rows\");\n const entry = seqScansByRelation.get(relation) ?? {\n self: 0,\n rows: undefined,\n topNode: node,\n topSelf: -1,\n };\n entry.self += self;\n // Keep the single heaviest contributing node so the cost-breakdown explainer\n // describes the scan that actually dominates, not an arbitrary one.\n if (self > entry.topSelf) {\n entry.topSelf = self;\n entry.topNode = node;\n }\n if (rows !== undefined) entry.rows = Math.max(entry.rows ?? 0, rows);\n seqScansByRelation.set(relation, entry);\n }\n for (const [relation, { self, rows, topNode }] of seqScansByRelation) {\n const share = clampShare(self, rootTotal);\n const costHeavy = share >= SEQ_SCAN_COST_SHARE;\n const rowHeavy = rows !== undefined && rows >= SEQ_SCAN_ROWS;\n if (!costHeavy && !rowHeavy) continue;\n const pct = Math.round(share * 100);\n const named = relation !== \"?\";\n // When the scan is selective, the clause states scanned-vs-returned in full,\n // so the bare \"(~N rows)\" — which shows the post-filter *output* and reads as\n // if the scan touched only that many — would contradict it. Drop it then.\n const selectivity = selectivityClause(topNode);\n const rowsText =\n !selectivity && rows !== undefined\n ? ` (~${rows.toLocaleString(\"en-US\")} rows)`\n : \"\";\n const scanned = selectivity ? rowsScanned(topNode) : undefined;\n findings.push({\n code: \"EXPENSIVE_SEQ_SCAN\",\n severity: \"warning\",\n impact: share,\n title: `Sequential scan${named ? ` on ${relation}` : \"\"}`,\n detail: `${\n named ? `\\`${relation}\\`` : \"A table\"\n } is read with a full sequential scan${rowsText}, about ${pct}% of the query's cost.${whyExpensive(topNode)}${selectivity}${scanIndexClause}`,\n evidence: {\n ...(named ? { relation } : {}),\n ...(rows !== undefined ? { rows } : {}),\n ...(scanned !== undefined ? { scanned } : {}),\n costShare: `${pct}%`,\n },\n });\n }\n\n // 3. Unbounded result that ships a wide row set on every call. The MISSING_LIMIT\n // nudge is the syntactic half; pairing it with the plan's estimated payload\n // turns it into a concrete \"you move ~N MB per call, and it grows with the\n // table\" verdict.\n const hasMissingLimit = nudges.some(\n (nudge) => nudge.kind === \"MISSING_LIMIT_CLAUSE\",\n );\n if (hasMissingLimit) {\n const rows = num(root, \"Plan Rows\");\n const width = num(root, \"Plan Width\");\n if (rows !== undefined && width !== undefined) {\n const bytes = rows * width;\n if (bytes >= WIDE_RESULT_BYTES) {\n findings.push({\n code: \"WIDE_RESULT_NO_LIMIT\",\n severity: bytes >= WIDE_RESULT_WARN_BYTES ? \"warning\" : \"info\",\n // Payload size on the cost scale: ~1 MB ≈ 0.5, saturating below 1 so a\n // big payload ranks alongside — but not above — a dominant cost node.\n impact: Math.min(0.9, bytes / 2_000_000),\n title: \"Unbounded result set\",\n detail: `No LIMIT, and the query returns ~${rows.toLocaleString(\n \"en-US\",\n )} rows of ~${width} bytes (~${formatBytes(\n bytes,\n )} total). That width is a planner estimate and undercounts jsonb and large-text columns, so the real payload is likely bigger. To shrink it, drop columns from the SELECT or bound the rows.`,\n evidence: {\n rows,\n rowWidth: width,\n estimatedPayload: formatBytes(bytes),\n },\n });\n }\n }\n }\n\n // 4. Connect the function-on-column nudge to its plan consequence — but only\n // for the table the function actually wraps. The nudge is AST-only (it knows a\n // function wraps some column, not which table), so naming every sequentially\n // scanned relation falsely blames tables scanned for unrelated reasons. Name\n // only relations that are both sequentially scanned AND appear inside a\n // function in a plan condition; with no such intersection there's no proven\n // plan consequence, so the finding stays silent (the nudge still stands).\n const hasFunctionOnColumn = nudges.some(\n (nudge) => nudge.kind === \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n );\n if (hasFunctionOnColumn) {\n const wrapped = relationsWrappedInFunction(root);\n const seqScanned = new Set(\n nodes\n .filter(({ node }) => str(node, \"Node Type\") === \"Seq Scan\")\n .map(({ node }) => str(node, \"Relation Name\"))\n .filter((relation): relation is string => relation !== undefined),\n );\n const blocked = [...seqScanned].filter((relation) => wrapped.has(relation));\n if (blocked.length > 0) {\n const relationList = blocked.map((r) => `\\`${r}\\``).join(\", \");\n // Rank by what the blocked scans actually cost — multiplied, since a\n // function on a tiny lookup table run once per row is a red herring next\n // to a dominant loop. This keeps the finding from looking as urgent as it\n // isn't (on the dogfood loader, ci_repo_configs is ~2%, not headline).\n const blockedCost = nodes\n .filter(\n ({ node }) =>\n str(node, \"Node Type\") === \"Seq Scan\" &&\n blocked.includes(str(node, \"Relation Name\") ?? \"\"),\n )\n .reduce((sum, { node }) => sum + multipliedCost(node), 0);\n findings.push({\n code: \"FUNCTION_ON_COLUMN_BLOCKS_INDEX\",\n severity: \"warning\",\n impact: clampShare(blockedCost, rootTotal),\n title: \"A function on a column blocks an index\",\n detail: `A condition wraps ${relationList}'s column in a function (e.g. \\`lower(col)\\`), so Postgres scans the table instead of using an index. Compare the bare column, or add an expression index for the function.${recommendedClause}`,\n evidence: { sequentialScans: relationList },\n });\n }\n }\n\n // 5. In-memory sorts that cost real money. Postgres can skip a sort entirely\n // when an index already returns rows in the wanted order, so a Sort node\n // carrying a meaningful share of the cost is a standing \"an index in this\n // order removes this\" signal. The dominant-node finding already names the\n // single worst node, so skip it here.\n const reportedSortKeys = new Set<string>();\n const topNSorts = sortsBoundedByLimit(root);\n for (const { node, self } of nodes) {\n if (str(node, \"Node Type\") !== \"Sort\") continue;\n if (reported.has(node)) continue;\n const share = clampShare(self, rootTotal);\n if (share < SORT_COST_SHARE) continue;\n const sortKey = sortKeyText(node);\n // The same ORDER BY can appear as twin sorts across subplans — report once.\n const dedupeKey = sortKey ?? \"\";\n if (reportedSortKeys.has(dedupeKey)) continue;\n reportedSortKeys.add(dedupeKey);\n const pct = Math.round(share * 100);\n // A Sort a LIMIT bounds is a top-N: a matching index doesn't just skip the\n // sort, it lets the scan stop after the first rows instead of ordering the\n // whole set — the stronger, more concrete win. Only when the key is actually\n // indexable (an aggregate/subquery sort key can't be pre-ordered either way).\n const advice = sortKeyIsIndexable(sortKey)\n ? topNSorts.has(node)\n ? \"An index in that order would skip the sort and, with the LIMIT, let Postgres stop after the first rows instead of ordering the whole set.\"\n : \"An index in that order would let Postgres skip the sort.\"\n : \"The sort key is computed at runtime (an aggregate or subquery result), so no index can pre-sort it.\";\n findings.push({\n code: \"SORT_WITHOUT_INDEX\",\n severity: share >= SORT_WARN_SHARE ? \"warning\" : \"info\",\n impact: share,\n title: \"In-memory sort\",\n detail: `Rows are sorted in memory${\n sortKey ? ` by \\`${sortKey}\\`` : \"\"\n }, about ${pct}% of the query's cost. ${advice}`,\n evidence: {\n ...(sortKey ? { sortKey } : {}),\n costShare: `${pct}%`,\n },\n });\n }\n\n // Dedup: a generic EXPENSIVE_SEQ_SCAN on a relation a more specific finding\n // already explains — the function that blocks its index, or a loop's per-row\n // driver — is the same scan said twice. Keep the specific one.\n const explained = new Set<string>();\n for (const finding of findings) {\n if (finding.code === \"FUNCTION_ON_COLUMN_BLOCKS_INDEX\") {\n const list = String(finding.evidence?.sequentialScans ?? \"\");\n for (const match of list.matchAll(/`([^`]+)`/g)) explained.add(match[1]);\n }\n if (finding.code === \"REPEATED_INNER_LOOP\" && finding.evidence?.driver) {\n explained.add(String(finding.evidence.driver));\n }\n }\n const deduped = findings.filter(\n (finding) =>\n !(\n finding.code === \"EXPENSIVE_SEQ_SCAN\" &&\n explained.has(String(finding.evidence?.relation ?? \"\"))\n ),\n );\n\n // Biggest lever first: a reader (or an agent) should act on the finding worth\n // the most, not whichever happened to be detected first.\n deduped.sort((a, b) => b.impact - a.impact);\n return deduped;\n}\n\n/** Whether an index could pre-order this sort. A plain column (or a function of\n * one) can be served by an index; a sort on an aggregate, a subquery result, or\n * a computed comparison is produced at runtime, so no index can pre-order it —\n * promising one would be false advice. Unknown keys keep the generic advice. */\nfunction sortKeyIsIndexable(sortKey: string | undefined): boolean {\n if (!sortKey) return true;\n if (/\\bSubPlan\\b/i.test(sortKey)) return false;\n if (\n /\\b(count|sum|avg|min|max|jsonb_agg|array_agg|string_agg|bool_or|bool_and|row_number|rank|dense_rank|ntile)\\s*\\(/i.test(\n sortKey,\n )\n ) {\n return false;\n }\n // A whitespace-delimited comparison, e.g. ((a = b)) — a computed boolean, not a\n // column. Requiring spaces around the operator avoids matching JSON path\n // operators (`->`, `->>`), which contain `>` but are indexable via expression\n // index.\n if (/\\s(=|<|>|<=|>=|<>)\\s/.test(sortKey)) return false;\n return true;\n}\n\n/** The Sort node's ORDER BY, as a compact string. `Sort Key` is a string array\n * (e.g. [\"created_at DESC\", \"id DESC\"]); join it and trim runaway length. */\nfunction sortKeyText(node: PlanNode): string | undefined {\n const raw = node[\"Sort Key\"];\n if (!Array.isArray(raw)) return undefined;\n const keys = raw.filter((k): k is string => typeof k === \"string\");\n if (keys.length === 0) return undefined;\n const joined = keys.join(\", \");\n return joined.length > 120 ? `${joined.slice(0, 117)}…` : joined;\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -17,6 +17,7 @@ const require_aggregate_index_recommendations = require("./action-plan/aggregate
17
17
  const require_build_action_plan = require("./action-plan/build-action-plan.cjs");
18
18
  const require_sentry = require("./sentry.cjs");
19
19
  const require_query = require("./query.cjs");
20
+ const require_query_findings = require("./findings/query-findings.cjs");
20
21
  exports.Analyzer = require_analyzer.Analyzer;
21
22
  exports.CombinedExport = require_dump.CombinedExport;
22
23
  exports.ComputedColumnStats = require_statistics.ComputedColumnStats;
@@ -57,6 +58,7 @@ exports.Statistics = require_statistics.Statistics;
57
58
  exports.StatisticsMode = require_statistics.StatisticsMode;
58
59
  exports.StatisticsSource = require_statistics.StatisticsSource;
59
60
  exports.aggregateIndexRecommendations = require_aggregate_index_recommendations.aggregateIndexRecommendations;
61
+ exports.analyzeQueryFindings = require_query_findings.analyzeQueryFindings;
60
62
  exports.buildActionPlan = require_build_action_plan.buildActionPlan;
61
63
  exports.combinedDumpSql = require_dump.combinedDumpSql;
62
64
  exports.compactSelectList = require_display_query.compactSelectList;
package/dist/index.d.cts CHANGED
@@ -18,4 +18,5 @@ import { ActionPlanQuery, aggregateIndexRecommendations } from "./action-plan/ag
18
18
  import { ActionPlanNudgeQuery, ActionableStep, AffectedQueryCost, CostReduction, DomainLabel, IndexAction, NudgeFinding, RegressionBreach, buildActionPlan } from "./action-plan/build-action-plan.cjs";
19
19
  import { deriveSentryEnvironment } from "./sentry.cjs";
20
20
  import { ClientApi, ConnectionMode, ExtensionPresence, IndexDefinition, RepoConfig, ServerApi, UnauthenticatedServerApi } from "./websocket-server.cjs";
21
- export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, parseNudges };
21
+ import { QueryFinding, QueryFindingCode, QueryFindingSeverity, analyzeQueryFindings } from "./findings/query-findings.cjs";
22
+ export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryFinding, QueryFindingCode, QueryFindingSeverity, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, parseNudges };
package/dist/index.d.mts CHANGED
@@ -18,4 +18,5 @@ import { ActionPlanQuery, aggregateIndexRecommendations } from "./action-plan/ag
18
18
  import { ActionPlanNudgeQuery, ActionableStep, AffectedQueryCost, CostReduction, DomainLabel, IndexAction, NudgeFinding, RegressionBreach, buildActionPlan } from "./action-plan/build-action-plan.mjs";
19
19
  import { deriveSentryEnvironment } from "./sentry.mjs";
20
20
  import { ClientApi, ConnectionMode, ExtensionPresence, IndexDefinition, RepoConfig, ServerApi, UnauthenticatedServerApi } from "./websocket-server.mjs";
21
- export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, parseNudges };
21
+ import { QueryFinding, QueryFindingCode, QueryFindingSeverity, analyzeQueryFindings } from "./findings/query-findings.mjs";
22
+ export { ActionPlanNudgeQuery, ActionPlanQuery, ActionableStep, AffectedQueryCost, AggregatedIndexRecommendation, AnalysisResult, Analyzer, ClientApi, ColumnMetadata, CombinedExport, CompactSelectListOptions, ComputedColumnStats, ComputedReltuples, ComputedStats, ConnectionMode, CostReduction, DUMP_STATS_SQL, DatabaseDriver, DiscoveredColumnReference, DomainLabel, DuplicateIndexGroup, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, ExtensionPresence, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexAction, IndexDefinition, IndexGroup, IndexIdentifier, IndexOptimizer, IndexRecommendation, IndexToCreate, JsonbOperator, LiveQueryOptimization, Nudge, NudgeFinding, OptimizationResult, OptimizeResult, OptimizedQuery, PROCEED, Parameter, Parser, Path, PermutedIndexCandidate, PgIdentifier, Postgres, PostgresConnectionInput, PostgresExplainResult, PostgresExplainStage, PostgresExplainStageCommon, PostgresExplainStageSchema, PostgresFactory, PostgresQueryBuilder, PostgresQueryBuilderCommand, PostgresStage, PostgresStageId, PostgresTransaction, PostgresVersion, PssRewriter, QueryFinding, QueryFindingCode, QueryFindingSeverity, QueryHash, QuerySource, RecentQuery, RegressionBreach, RepoConfig, RootIndexCandidate, SKIP, SQLCommenterExtraction, SQLCommenterTag, SerializeResult, ServerApi, SortContext, StatementType, Statistics, StatisticsMode, StatisticsSource, TableMetadata, TableReference, TableStats, UnauthenticatedServerApi, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, parseNudges };
package/dist/index.mjs CHANGED
@@ -16,4 +16,5 @@ import { aggregateIndexRecommendations } from "./action-plan/aggregate-index-rec
16
16
  import { buildActionPlan } from "./action-plan/build-action-plan.mjs";
17
17
  import { deriveSentryEnvironment } from "./sentry.mjs";
18
18
  import { OptimizedQuery, RecentQuery } from "./query.mjs";
19
- export { Analyzer, CombinedExport, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexOptimizer, OptimizedQuery, PROCEED, PgIdentifier, PostgresExplainStageSchema, PostgresQueryBuilder, PostgresVersion, PssRewriter, RecentQuery, SKIP, Statistics, StatisticsMode, StatisticsSource, aggregateIndexRecommendations, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, parseNudges };
19
+ import { analyzeQueryFindings } from "./findings/query-findings.mjs";
20
+ export { Analyzer, CombinedExport, ComputedColumnStats, ComputedReltuples, ComputedStats, DUMP_STATS_SQL, ExportedQuery, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, FullSchema, FullSchemaColumn, FullSchemaCompositeAttribute, FullSchemaConstraint, FullSchemaExtension, FullSchemaFunction, FullSchemaIncludedColumn, FullSchemaIndex, FullSchemaKeyColumn, FullSchemaTable, FullSchemaTrigger, FullSchemaType, FullSchemaTypeConstraint, FullSchemaView, IndexOptimizer, OptimizedQuery, PROCEED, PgIdentifier, PostgresExplainStageSchema, PostgresQueryBuilder, PostgresVersion, PssRewriter, RecentQuery, SKIP, Statistics, StatisticsMode, StatisticsSource, aggregateIndexRecommendations, analyzeQueryFindings, buildActionPlan, combinedDumpSql, compactSelectList, deriveSentryEnvironment, dropIndex, dumpQueriesSql, dumpSchema, groupDuplicateIndexes, groupIndexesByCoverage, ignoredIdentifier, indexCovers, isIndexProbablyDroppable, isIndexSupported, parseNudges };
@@ -69,13 +69,16 @@ const ExportedStatsV1 = zod.z.object({
69
69
  indexes: zod.z.array(ExportedStatsIndex)
70
70
  });
71
71
  const ExportedStats = zod.z.union([ExportedStatsV1]);
72
+ const ScaleFactor = zod.z.number().min(1).optional();
72
73
  const StatisticsMode = zod.z.discriminatedUnion("kind", [zod.z.object({
73
74
  kind: zod.z.literal("fromAssumption"),
74
- reltuples: zod.z.number().min(0)
75
+ reltuples: zod.z.number().min(0),
76
+ scale: ScaleFactor
75
77
  }), zod.z.object({
76
78
  kind: zod.z.literal("fromStatisticsExport"),
77
79
  stats: zod.z.array(ExportedStats),
78
- source: StatisticsSource
80
+ source: StatisticsSource,
81
+ scale: ScaleFactor
79
82
  })]);
80
83
  const ComputedColumnStats = zod.z.object({
81
84
  schema_name: zod.z.string(),
@@ -271,6 +274,9 @@ var Statistics = class Statistics {
271
274
  buildComputedStats() {
272
275
  const columnStats = [];
273
276
  const reltuples = [];
277
+ const scale = this.mode.scale ?? 1;
278
+ const scaleTuples = (value) => value * scale;
279
+ const scalePages = (value) => Math.ceil(value * scale);
274
280
  for (const table of this.ownMetadata) {
275
281
  const targetTable = this.exportedMetadata?.find((m) => m.tableName === table.tableName && m.schemaName === table.schemaName);
276
282
  const target = targetTable?.columns ?? table.columns;
@@ -380,29 +386,29 @@ var Statistics = class Statistics {
380
386
  reltuples.push({
381
387
  relname: table.tableName,
382
388
  schema_name: table.schemaName,
383
- reltuples: tableReltuples,
384
- relpages: tableRelpages,
389
+ reltuples: scaleTuples(tableReltuples),
390
+ relpages: scalePages(tableRelpages),
385
391
  relallfrozen,
386
- relallvisible
392
+ relallvisible: scalePages(relallvisible)
387
393
  });
388
394
  if (this.mode.kind === "fromAssumption") for (const index of table.indexes) {
389
395
  const indexRelpages = estimateIndexRelpages(this.mode.reltuples, index.columns, index.fillfactor / 100, index.amname, tableRelpages);
390
396
  reltuples.push({
391
397
  relname: index.indexName,
392
398
  schema_name: table.schemaName,
393
- reltuples: this.mode.reltuples,
394
- relpages: indexRelpages,
399
+ reltuples: scaleTuples(this.mode.reltuples),
400
+ relpages: scalePages(indexRelpages),
395
401
  relallfrozen: 0,
396
- relallvisible: indexRelpages
402
+ relallvisible: scalePages(indexRelpages)
397
403
  });
398
404
  }
399
405
  else if (targetTable) for (const index of targetTable.indexes) reltuples.push({
400
406
  relname: index.indexName,
401
407
  schema_name: targetTable.schemaName,
402
- reltuples: index.reltuples,
403
- relpages: index.relpages,
408
+ reltuples: scaleTuples(index.reltuples),
409
+ relpages: scalePages(index.relpages),
404
410
  relallfrozen: index.relallfrozen,
405
- relallvisible: index.relallvisible
411
+ relallvisible: scalePages(index.relallvisible)
406
412
  });
407
413
  }
408
414
  return {
@@ -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 // mark all pages as visible to make sure index only scans\n // don't assume they'll have to do 100% heap fetches\n relallvisible = tableRelpages;\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 relallvisible = 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 [JSON.stringify(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\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"],"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\n// `scale` multiplies the planner's view of table/index size (reltuples,\n// relpages, relallvisible) so a query can be planned at N× the current data\n// size. It is left optional rather than `.default(1)` so historical persisted\n// modes (e.g. older CI runs) without the field keep parsing unchanged; absence\n// is treated as 1 wherever it's read. pg_statistic column stats are left\n// untouched — they are scale-invariant by Postgres design.\nconst ScaleFactor = z.number().min(1).optional();\n\nexport const StatisticsMode = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"fromAssumption\"),\n reltuples: z.number().min(0),\n scale: ScaleFactor,\n }),\n z.object({\n kind: z.literal(\"fromStatisticsExport\"),\n stats: z.array(ExportedStats),\n source: StatisticsSource,\n scale: ScaleFactor,\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 // Show the planner the database at `scale`× its real size. Applied\n // uniformly to every code path here; only the export mode surfaces `scale`\n // in the UI/config layers (for fromAssumption, `scale × reltuples` is\n // redundant since `reltuples` is already an explicit assumption).\n const scale = this.mode.scale ?? 1;\n // reltuples is a real, but relpages/relallvisible are integer columns.\n const scaleTuples = (value: number): number => value * scale;\n const scalePages = (value: number): number => Math.ceil(value * scale);\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 // mark all pages as visible to make sure index only scans\n // don't assume they'll have to do 100% heap fetches\n relallvisible = tableRelpages;\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 relallvisible = DEFAULT_RELPAGES;\n }\n\n reltuples.push({\n relname: table.tableName,\n schema_name: table.schemaName,\n reltuples: scaleTuples(tableReltuples),\n relpages: scalePages(tableRelpages),\n relallfrozen,\n relallvisible: scalePages(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: scaleTuples(this.mode.reltuples),\n relpages: scalePages(indexRelpages),\n relallfrozen: 0,\n relallvisible: scalePages(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: scaleTuples(index.reltuples),\n relpages: scalePages(index.relpages),\n relallfrozen: index.relallfrozen,\n relallvisible: scalePages(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 [JSON.stringify(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\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"],"mappings":";;;;;;;;;AAeA,MAAa,mBAAmBA,IAAAA,EAAE,MAAM,CACtCA,IAAAA,EAAE,OAAO;CACP,MAAMA,IAAAA,EAAE,QAAQ,OAAO;CACvB,MAAMA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CACxB,CAAC,EACFA,IAAAA,EAAE,OAAO,EACP,MAAMA,IAAAA,EAAE,QAAQ,SAAS,EAC1B,CAAC,CACH,CAAC;AAEF,MAAa,0BAA0BA,IAAAA,EAAE,OAAO;CAC9C,UAAUA,IAAAA,EAAE,QAAQ;CACpB,YAAYA,IAAAA,EAAE,SAAS,CAAC,QAAQ,MAAM;CAEtC,aAAaA,IAAAA,EAAE,QAAQ;CAEvB,aAAaA,IAAAA,EAAE,QAAQ;CACvB,UAAUA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,UAAUA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,UAAUA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,UAAUA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,UAAUA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAG3C,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACxC,CAAC;AAEF,MAAa,uBAAuBA,IAAAA,EAAE,OAAO;CAC3C,YAAYA,IAAAA,EAAE,QAAQ;CACtB,QAAQA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CAC7B,UAAUA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CAC/B,OAAO,wBAAwB,UAAU;CAC1C,CAAC;AAEF,MAAa,qBAAqBA,IAAAA,EAAE,OAAO;CACzC,WAAWA,IAAAA,EAAE,QAAQ;CACrB,QAAQA,IAAAA,EAAE,QAAQ,CAAC,QAAQ,QAAQ;CACnC,UAAUA,IAAAA,EAAE,QAAQ;CACpB,WAAWA,IAAAA,EAAE,QAAQ;CACrB,eAAeA,IAAAA,EAAE,QAAQ;CACzB,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,YAAYA,IAAAA,EAAE,QAAQ,CAAC,QAAQ,GAAG;CAClC,SAASA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,OAAO,EAAE,QAAQA,IAAAA,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC1E,CAAC;AAKF,MAAa,kBAAkBA,IAAAA,EAAE,OAAO;CACtC,WAAWA,IAAAA,EAAE,QAAQ;CACrB,YAAYA,IAAAA,EAAE,QAAQ;CAEtB,UAAUA,IAAAA,EAAE,QAAQ;CAEpB,WAAWA,IAAAA,EAAE,QAAQ;CACrB,eAAeA,IAAAA,EAAE,QAAQ;CAEzB,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,SAASA,IAAAA,EAAE,MAAM,qBAAqB,CAAC,QAAQ,EAAE,CAAC;CAClD,SAASA,IAAAA,EAAE,MAAM,mBAAmB;CACrC,CAAC;AAEF,MAAa,gBAAgBA,IAAAA,EAAE,MAAM,CAAC,gBAAgB,CAAC;AAUvD,MAAM,cAAcA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;AAEhD,MAAa,iBAAiBA,IAAAA,EAAE,mBAAmB,QAAQ,CACzDA,IAAAA,EAAE,OAAO;CACP,MAAMA,IAAAA,EAAE,QAAQ,iBAAiB;CACjC,WAAWA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC5B,OAAO;CACR,CAAC,EACFA,IAAAA,EAAE,OAAO;CACP,MAAMA,IAAAA,EAAE,QAAQ,uBAAuB;CACvC,OAAOA,IAAAA,EAAE,MAAM,cAAc;CAC7B,QAAQ;CACR,OAAO;CACR,CAAC,CACH,CAAC;AAIF,MAAa,sBAAsBA,IAAAA,EAAE,OAAO;CAC1C,aAAaA,IAAAA,EAAE,QAAQ;CACvB,YAAYA,IAAAA,EAAE,QAAQ;CACtB,aAAaA,IAAAA,EAAE,QAAQ;CACvB,WAAWA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CAChC,YAAYA,IAAAA,EAAE,SAAS;CACvB,aAAaA,IAAAA,EAAE,QAAQ;CACvB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,aAAaA,IAAAA,EAAE,QAAQ;CACvB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,QAAQA,IAAAA,EAAE,QAAQ;CAClB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,aAAaA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC3C,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,YAAYA,IAAAA,EAAE,MAAMA,IAAAA,EAAE,KAAK,CAAC,CAAC,UAAU;CACvC,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACnC,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACpC,CAAC;AAIF,MAAa,oBAAoBA,IAAAA,EAAE,OAAO;CACxC,SAASA,IAAAA,EAAE,QAAQ;CACnB,aAAaA,IAAAA,EAAE,QAAQ;CACvB,WAAWA,IAAAA,EAAE,QAAQ;CACrB,UAAUA,IAAAA,EAAE,QAAQ;CACpB,eAAeA,IAAAA,EAAE,QAAQ;CACzB,cAAcA,IAAAA,EAAE,QAAQ,CAAC,UAAU;CACpC,CAAC;AAIF,MAAa,gBAAgBA,IAAAA,EAAE,OAAO;CACpC,aAAaA,IAAAA,EAAE,MAAM,oBAAoB;CACzC,WAAWA,IAAAA,EAAE,MAAM,kBAAkB;CACtC,CAAC;AAIF,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AAEzB,MAAM,oBAAoB,KAAK;AAE/B,SAAS,iBAAiB,KAAyC;AACjE,QAAO,IAAI,UAAU;;AAGvB,SAAS,iBACP,WACA,SACQ;CACR,MAAM,WAEJ,QAAQ,QAAQ,KAAK,QAAQ,MAAM,iBAAiB,IAAI,EAAE,EAAE,GAAG;AACjE,QAAO,KAAK,KAAM,YAAY,WAAY,kBAAkB;;AAG9D,SAAS,sBACP,WACA,SACA,YACA,QACA,eACQ;AACR,KAAI,WAAW,MAGb,QAAO,KAAK,KAAK,gBAAgB,GAAI;CAGvC,MAAM,WAAW,QAAQ,QACtB,KAAK,QAAQ,MAAM,iBAAiB,IAAI,GAAG,IAC5C,EACD;AACD,QAAO,KAAK,KAAM,YAAY,WAAY,oBAAoB,WAAW;;AAG3E,MAAa,iBAAiB,OAAA,OAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GpC,IAAa,aAAb,MAAa,WAAW;CAUtB,YACE,IACA,iBACA,aACA,WACA;AAJiB,OAAA,KAAA;AACD,OAAA,kBAAA;AACA,OAAA,cAAA;+CAZT,QAAA,KAAA,EAAqB;+CACrB,iBAAA,KAAA,EAA6B;+CACrB,oBAAA,KAAA,EAA8C;+CACvD,qBAAqC,EAAE,CAAC;AAY9C,MAAI,WAAW;AACb,QAAK,OAAO;AACZ,OAAI,UAAU,SAAS,uBACrB,MAAK,mBAAmB,UAAU;QAGpC,MAAK,OAAO,WAAW;AAEzB,OAAK,gBAAgB,KAAK,oBAAoB;;CAGhD,qBAAqB,mBAA0C;AAC7D,OAAK,oBAAoB;;CAG3B,qBAA4C;EAC1C,MAAM,cAAqC,EAAE;EAC7C,MAAM,YAAiC,EAAE;EAMzC,MAAM,QAAQ,KAAK,KAAK,SAAS;EAEjC,MAAM,eAAe,UAA0B,QAAQ;EACvD,MAAM,cAAc,UAA0B,KAAK,KAAK,QAAQ,MAAM;AAEtE,OAAK,MAAM,SAAS,KAAK,aAAa;GACpC,MAAM,cAAc,KAAK,kBAAkB,MACxC,MACC,EAAE,cAAc,MAAM,aAAa,EAAE,eAAe,MAAM,WAC7D;GACD,MAAM,SAAS,aAAa,WAAW,MAAM;AAE7C,QAAK,MAAM,UAAU,QAAQ;IAC3B,MAAM,EAAE,UAAU;AAClB,QAAI,CAAC,SAAS,KAAK,KAAK,SAAS,kBAAkB;KACjD,MAAM,WAAW,OAAO,YAAY,iBAAiB,OAAO;AAC5D,iBAAY,KAAK;MACf,aAAa,MAAM;MACnB,YAAY,MAAM;MAClB,aAAa,OAAO;MACpB,WAAW,OAAO;MAClB,YAAY;MACZ,aAAa;MACb;MACA,aAAa;MACb,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,UAAU;MACV,QAAQ;MACR,QAAQ;MACR,QAAQ;MACR,QAAQ;MACR,QAAQ;MACR,aAAa;MACb,aAAa;MACb,aAAa,CAAC,GAAI;MAClB,aAAa;MACb,aAAa;MACb,YAAY;MACZ,YAAY;MACZ,YAAY;MACZ,YAAY;MACZ,YAAY;MACZ,cAAc;MACd,cAAc;MACd,cAAc;MACd,cAAc;MACd,cAAc;MACf,CAAC;UAEF,aAAY,KAAK;KACf,aAAa,MAAM;KACnB,YAAY,MAAM;KAClB,aAAa,OAAO;KACpB,WAAW,OAAO;KAClB,YAAY,MAAM,cAAc;KAChC,aAAa,MAAM;KACnB,UAAU,MAAM;KAChB,aAAa,MAAM;KACnB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,QAAQ,MAAM;KACd,QAAQ,MAAM;KACd,QAAQ,MAAM;KACd,QAAQ,MAAM;KACd,QAAQ,MAAM;KACd,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,aAAa,MAAM;KACnB,aAAa,MAAM;KACnB,aAAa,MAAM;KACnB,aAAa,MAAM;KACnB,aAAa,MAAM;KACnB,YAAY,WAAW,cAAc,MAAM,WAAW;KACtD,YAAY,WAAW,cAAc,MAAM,WAAW;KACtD,YAAY,WAAW,cAAc,MAAM,WAAW;KACtD,YAAY,WAAW,cAAc,MAAM,WAAW;KACtD,YAAY,WAAW,cAAc,MAAM,WAAW;KACtD,cAAc,KAAK,aACjB,WAAW,cAAc,MAAM,WAAW,CAC3C;KACD,cAAc,KAAK,aACjB,WAAW,cAAc,MAAM,WAAW,CAC3C;KACD,cAAc,KAAK,aACjB,WAAW,cAAc,MAAM,WAAW,CAC3C;KACD,cAAc,KAAK,aACjB,WAAW,cAAc,MAAM,WAAW,CAC3C;KACD,cAAc,KAAK,aACjB,WAAW,cAAc,MAAM,WAAW,CAC3C;KACF,CAAC;;GAIN,IAAI;GACJ,IAAI;GACJ,IAAI,gBAAgB;GACpB,IAAI;AAEJ,OAAI,KAAK,KAAK,SAAS,kBAAkB;AACvC,qBAAiB,KAAK,KAAK;AAC3B,oBAAgB,iBAAiB,gBAAgB,MAAM,QAAQ;AAG/D,oBAAgB;cACP,aAAa;AACtB,qBAAiB,YAAY;AAC7B,oBAAgB,YAAY;AAC5B,oBAAgB,YAAY;AAC5B,mBAAe,YAAY;UACtB;AACL,qBAAiB;AACjB,oBAAgB;AAChB,oBAAgB;;AAGlB,aAAU,KAAK;IACb,SAAS,MAAM;IACf,aAAa,MAAM;IACnB,WAAW,YAAY,eAAe;IACtC,UAAU,WAAW,cAAc;IACnC;IACA,eAAe,WAAW,cAAc;IACzC,CAAC;AAEF,OAAI,KAAK,KAAK,SAAS,iBACrB,MAAK,MAAM,SAAS,MAAM,SAAS;IACjC,MAAM,gBAAgB,sBACpB,KAAK,KAAK,WACV,MAAM,SACN,MAAM,aAAa,KACnB,MAAM,QACN,cACD;AACD,cAAU,KAAK;KACb,SAAS,MAAM;KACf,aAAa,MAAM;KACnB,WAAW,YAAY,KAAK,KAAK,UAAU;KAC3C,UAAU,WAAW,cAAc;KACnC,cAAc;KACd,eAAe,WAAW,cAAc;KACzC,CAAC;;YAEK,YACT,MAAK,MAAM,SAAS,YAAY,QAC9B,WAAU,KAAK;IACb,SAAS,MAAM;IACf,aAAa,YAAY;IACzB,WAAW,YAAY,MAAM,UAAU;IACvC,UAAU,WAAW,MAAM,SAAS;IACpC,cAAc,MAAM;IACpB,eAAe,WAAW,MAAM,cAAc;IAC/C,CAAC;;AAKR,SAAO;GAAE;GAAa;GAAW;;CAGnC,OAAO,wBAAwB,EAC7B,aAGiB;AACjB,SAAO;GACL,MAAM;GACN;GACD;;;;;CAMH,OAAO,oBAAoB,OAAwC;AACjE,SAAO;GACL,MAAM;GACN,QAAQ,EAAE,MAAM,UAAU;GAC1B;GACD;;CAGH,aAAa,aACX,IACA,WACqB;EACrB,MAAM,UAAU,MAAM,GAAG,WAAW;AAEpC,SAAO,IAAI,WAAW,IAAI,SADT,MAAM,WAAW,UAAU,IAAI,QAAQ,EACX,UAAU;;CAGzD,aAAa,IAAyB;AAEpC,SAAO,KAAK,eAAe,GAAG;;CAKhC,uBAAuB;AACrB,MAAI,CAAC,KAAK,iBACR,QAAO;EAET,IAAI,YAAY;AAChB,OAAK,MAAM,SAAS,KAAK,iBACvB,cAAa,MAAM;AAErB,SAAO;;;;;;CAOT,aAAqB,QAAwC;AAC3D,MAAI,CAAC,UAAU,OAAO,WAAW,EAC/B,QAAO;EAET,MAAM,CAAC,QAAQ;AACf,MAAI,OAAO,SAAS,SAClB,QAAO;WACE,OAAO,SAAS,UACzB,QAAO;AAIT,SAAO;;;;;;;;;CAUT,OAAe,cAAc,QAA4C;AACvE,MAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAC3C,MAAI,OAAO,MAAM,MAAM,MAAM,QAAQ,EAAE,CAAC,EAAE;AACxC,WAAQ,KAAK,qDAAqD;AAClE,UAAO;;AAET,SAAO;;;;;;;;;;;;;CAcT,MAAc,4BACZ,IAC8B;AAC9B,MAAI,KAAK,kBAAkB,WAAW,EACpC,QAAO,EAAE;EAYX,MAAM,aAAa,KAAK,kBAAkB,KAAK,QAAQ,IAAI,KAAK,UAAU,CAAC;EAC3E,MAAM,OAAO,MAAM,GAChB,KACC,OAAA,OAAM;;;;;;;;;;;;;;;;;;;;;;WAuBN,CAAC,KAAK,UAAU,WAAW,CAAC,CAC7B,CACA,OAAO,QAAQ;AACd,WAAQ,MACN,iEACD;AACD,WAAQ,MAAM,IAAI;AAClB,UAAO,EAAE;IACT;EAEJ,MAAM,8BAAc,IAAI,KAA4B;EACpD,MAAM,oCAAoB,IAAI,KAAqB;AACnD,OAAK,MAAM,OAAO,MAAM;AACtB,eAAY,IACV,GAAG,IAAI,YAAY,GAAG,IAAI,WAAW,GAAG,IAAI,eAC5C,IAAI,OACL;AACD,qBAAkB,IAAI,IAAI,YAAY,IAAI,WAAW;;AAGvD,SAAO,KAAK,kBAAkB,SAAS,QAAQ;GAC7C,MAAM,aAAa,KAAK,cAAc,UAAU,MAAM,MAAM;IAE1D,MAAM,eAAeC,sBAAAA,aAAa,WAAW,EAAE,YAAY,CAAC,UAAU;IACtE,MAAM,cAAcA,sBAAAA,aAAa,WAAW,EAAE,QAAQ,CAAC,UAAU;IAEjE,MAAM,eAAeA,sBAAAA,aAAa,WAAW,IAAI,OAAO,CAAC,UAAU;IACnE,MAAM,cAAcA,sBAAAA,aAAa,WAAW,IAAI,MAAM,CAAC,UAAU;AACjE,WAAO,iBAAiB,gBAAgB,gBAAgB;KACxD;AAEF,OAAI,CAAC,WACH,QAAO,EAAE;GAEX,MAAM,UAAU,IAAI,QAAQ,KAAK,SAAS,EACxC,QACE,YAAY,IAAI,GAAG,IAAI,OAAO,GAAG,IAAI,MAAM,GAAG,IAAI,SAAS,IAAI,MAClE,EAAE;GACH,MAAM,aAAa,kBAAkB,IAAI,IAAI,KAAK,UAAU,CAAC,IAAI;GACjE,MAAM,SAAS,IAAI,eAAe;GAClC,MAAM,WAAW,sBACf,WAAW,WACX,SACA,aAAa,KACb,QACA,WAAW,SACZ;AAED,UAAO,CACL;IACE,SAAS,IAAI,KAAK,UAAU;IAC5B,aAAa,IAAI;IACjB,WAAW,WAAW;IACtB;IACA,eAAe;IACf,cAAc;IACf,CACF;IACD;;CAGJ,MAAc,eAAe,IAAyB;EACpD,MAAM,WAAW;GACf,oBAAoB,EAAE;GACtB,iBAAiB,EAAE;GACnB,kBAAkB,EAAE;GACpB,cAAc,EAAE;GAMjB;EAED,MAAM,kBAAkB,IAAI,IAC1B,KAAK,YAAY,KAAK,MAAM,GAAG,EAAE,WAAW,GAAG,EAAE,YAAY,CAC9D;EAED,MAAM,2BAA2B,GAC9B,KAAK,WAAW,gBAAgB,CAAC,KAAK,cAAc,YAAY,CAAC,CACjE,OAAO,QAAQ;AACd,WAAQ,MAAM,8CAA8C;AAC5D,WAAQ,MAAM,IAAI;AAClB,SAAM;IACN;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BJ,MAAM,iBAAiB,OAAA,OAAM;;;;;;;;;;;;EAa7B,MAAM,2BAA2B,MAAM,KAAK,4BAA4B,GAAG;EAE3E,MAAM,mBAAmB,GACtB,KAAK,gBAAgB,CACpB,CAAC,GAAG,KAAK,cAAc,WAAW,GAAG,yBAAyB,CAC/D,CAAC,CACD,OAAO,QAAQ;AACd,WAAQ,MAAM,mDAAmD;AACjE,WAAQ,MAAM,IAAI;AAClB,UAAO;IACP;AAEJ,MAAI,KAAK,iBACP,MAAK,MAAM,SAAS,KAAK,kBAAkB;GACzC,MAAM,cAAc,gBAAgB,IAClC,GAAG,MAAM,WAAW,GAAG,MAAM,YAC9B;AACD,OAAI,eAAe,MAAM,cAAc,IAAI;AACzC,YAAQ,KACN,SAAS,MAAM,UAAU,+EAC1B;AACD,aAAS,iBAAiB,KACxB,GAAG,MAAM,WAAW,GAAG,MAAM,YAC9B;;AAEH,OAAI,YACF;AAEF,YAAS,gBAAgB,KAAK,GAAG,MAAM,WAAW,GAAG,MAAM,YAAY;;EAG3E,MAAM,CAAC,cAAc,oBAAoB,MAAM,QAAQ,IAAI,CACzD,0BACA,iBACD,CAAC;AAIF,MAAI,EAH2B,eAC3B,aAAa,WAAW,KAAK,cAAc,YAAY,SACvD,MAEF,SAAQ,MAAM,uCAAuC;AAEvD,MAAI,iBAAiB,WAAW,KAAK,cAAc,UAAU,OAC3D,SAAQ,MAAM,6CAA6C;AAE7D,SAAO;;CA2LT,aAAa,UACX,IACA,iBAC0B;AAC1B,UAAQ,IAAI,+BAAA,GAAA,UAAA,MAAmC,gBAAgB,GAAG;EAClE,MAAM,QAAQ,MAAM,GAAG,KAAoC,eAAe;AAE1E,SADYD,IAAAA,EAAE,MAAM,cAAc,CAAC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;;;mDAnsBnD,oBAAmC,OAAO,OAAO;CAC/D,MAAM;CACN,WAAW;CACZ,CAAC,CAAC;mDAkgBqB,kBAAiB,OAAA,OAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oFAsLmC"}
@@ -207,6 +207,7 @@ type ExportedStats = z.infer<typeof ExportedStats>;
207
207
  declare const StatisticsMode: z.ZodDiscriminatedUnion<[z.ZodObject<{
208
208
  kind: z.ZodLiteral<"fromAssumption">;
209
209
  reltuples: z.ZodNumber;
210
+ scale: z.ZodOptional<z.ZodNumber>;
210
211
  }, z.core.$strip>, z.ZodObject<{
211
212
  kind: z.ZodLiteral<"fromStatisticsExport">;
212
213
  stats: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
@@ -271,6 +272,7 @@ declare const StatisticsMode: z.ZodDiscriminatedUnion<[z.ZodObject<{
271
272
  }, z.core.$strip>, z.ZodObject<{
272
273
  kind: z.ZodLiteral<"inline">;
273
274
  }, z.core.$strip>]>;
275
+ scale: z.ZodOptional<z.ZodNumber>;
274
276
  }, z.core.$strip>], "kind">;
275
277
  type StatisticsMode = z.infer<typeof StatisticsMode>;
276
278
  declare const ComputedColumnStats: z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"statistics.d.cts","names":[],"sources":["../../src/optimizer/statistics.ts"],"mappings":";;;;;;;KAaY,IAAA;AAAA,cAEC,gBAAA,EAAgB,CAAA,CAAA,QAAA,WAAA,CAAA,CAAA,SAAA;;;;;;cAUhB,uBAAA,EAAuB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoCvB,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAOpB,kBAAA,EAAkB,CAAA,CAAA,SAAA;;;;;;;;;;;;cAclB,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAcf,aAAA,EAAa,CAAA,CAAA,QAAA,WAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAEd,aAAA,GAAgB,CAAA,CAAE,KAAA,QAAa,aAAA;AAAA,cAE9B,cAAA,EAAc,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAYf,cAAA,GAAiB,CAAA,CAAE,KAAA,QAAa,cAAA;AAAA,cAE/B,mBAAA,EAAmB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyCpB,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,mBAAA;AAAA,cAEpC,iBAAA,EAAiB,CAAA,CAAA,SAAA;;;;;;;;KASlB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,iBAAA;AAAA,cAElC,aAAA,EAAa,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAKd,aAAA,GAAgB,CAAA,CAAE,KAAA,QAAa,aAAA;AAAA,cAyC9B,cAAA;AAAA,cA2GA,UAAA;EAAA,iBAWQ,EAAA;EAAA,SACD,eAAA,EAAiB,eAAA;EAAA,SACjB,WAAA,EAAa,aAAA;EAAA,SAZtB,IAAA,EAAM,cAAA;EAAA,SACN,aAAA,EAAe,aAAA;EAAA,iBACP,gBAAA;EAAA,QACT,iBAAA;EAAA,gBAEQ,gBAAA,EAAkB,cAAA;cAKf,EAAA,EAAI,QAAA,EACL,eAAA,EAAiB,eAAA,EACjB,WAAA,EAAa,aAAA,IAC7B,SAAA,EAAW,cAAA;EAab,oBAAA,CAAqB,iBAAA,EAAmB,aAAA;EAAA,QAIhC,kBAAA;EAAA,OA+KD,uBAAA,CAAA;IACL;EAAA;IAEA,SAAA;EAAA,IACE,cAAA;;;;SAUG,mBAAA,CAAoB,KAAA,EAAO,aAAA,KAAkB,cAAA;EAAA,OAQvC,YAAA,CACX,EAAA,EAAI,QAAA,EACJ,SAAA,EAAW,cAAA,GACV,OAAA,CAAQ,UAAA;EAMX,YAAA,CAAa,EAAA,EAAI,mBAAA,GAAmB,OAAA;;;;;MA6K9B,SAAA;MACA,KAAA;MACA,MAAA;MACA,MAAA;IAAA;EAAA;EAzKN,oBAAA,CAAA;;;;;UAeQ,YAAA;;;;;;;;iBAsBO,aAAA;;;;;;;;;;;;UAoBD,2BAAA;EAAA,QAuGA,cAAA;EAAA,wBA8GU,cAAA;EAAA,OAwLX,SAAA,CACX,EAAA,EAAI,mBAAA,EACJ,eAAA,EAAiB,eAAA,GAChB,OAAA,CAAQ,aAAA;AAAA;AAAA,KAQD,cAAA;EACV,UAAA;EACA,QAAA;EACA,UAAA;EACA,KAAA,EAAO,WAAA;AAAA;AAAA,KAGJ,WAAA;EACH,UAAA;EACA,WAAA;EACA,QAAA;EACA,WAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,WAAA;EACA,WAAA;EACA,WAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,KAGU,aAAA;EACV,SAAA;EACA,UAAA;EACA,SAAA;EACA,QAAA;EACA,aAAA;EACA,YAAA;EACA,OAAA,EAAS,cAAA;AAAA;AAAA,KAGN,SAAA;AAAA,KACO,UAAA;EACV,aAAA;EACA,SAAA;AAAA;AAAA,KAGU,eAAA;EACV,MAAA,EAAQ,aAAA;EACR,UAAA;EACA,cAAA,EAAgB,MAAA,CAAO,SAAA;AAAA"}
1
+ {"version":3,"file":"statistics.d.cts","names":[],"sources":["../../src/optimizer/statistics.ts"],"mappings":"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"}
@@ -207,6 +207,7 @@ type ExportedStats = z.infer<typeof ExportedStats>;
207
207
  declare const StatisticsMode: z.ZodDiscriminatedUnion<[z.ZodObject<{
208
208
  kind: z.ZodLiteral<"fromAssumption">;
209
209
  reltuples: z.ZodNumber;
210
+ scale: z.ZodOptional<z.ZodNumber>;
210
211
  }, z.core.$strip>, z.ZodObject<{
211
212
  kind: z.ZodLiteral<"fromStatisticsExport">;
212
213
  stats: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
@@ -271,6 +272,7 @@ declare const StatisticsMode: z.ZodDiscriminatedUnion<[z.ZodObject<{
271
272
  }, z.core.$strip>, z.ZodObject<{
272
273
  kind: z.ZodLiteral<"inline">;
273
274
  }, z.core.$strip>]>;
275
+ scale: z.ZodOptional<z.ZodNumber>;
274
276
  }, z.core.$strip>], "kind">;
275
277
  type StatisticsMode = z.infer<typeof StatisticsMode>;
276
278
  declare const ComputedColumnStats: z.ZodObject<{