{"version":3,"file":"synthesize-reltuples.cjs","names":[],"sources":["../../src/optimizer/synthesize-reltuples.ts"],"sourcesContent":["import type {\n  FullSchema,\n  FullSchemaConstraint,\n  FullSchemaTable,\n} from \"../schema.ts\";\nimport type { PgIdentifier } from \"../sql/pg-identifier.ts\";\nimport type { ExportedStats } from \"./statistics.ts\";\n\n/**\n * Estimates a row count for every table in the current schema that the\n * production-stats snapshot doesn't cover — a table added since the snapshot was\n * captured (a local migration, or a CI branch). The snapshot's source has never\n * held these tables, so there is no real cardinality to read; we extrapolate one\n * from the tables the snapshot *does* know, anchoring on the foreign-key graph.\n *\n * The ladder, per uncovered table (see ADR 0007):\n *   1. FK to a covered parent that has other covered children → parent rows ×\n *      the median child/parent ratio of those siblings.\n *   2. FK to a covered parent with no other children → parent rows × the\n *      database-wide median child/parent ratio across all covered FK edges.\n *   3. No usable FK anchor, but a recognizable shape (an event log, a lookup) →\n *      the median size of the covered tables of that shape.\n *   4. No anchor and no matching shape → the database-wide median table size.\n *\n * FK *topology* comes from `FullSchema.constraints` (the current schema); every\n * row *count* comes from the snapshot, so an estimate is always a fact about\n * this database rather than a constant.\n *\n * Returns a map keyed by `\"schema.table\"` (both identifiers unquoted) holding the\n * synthesized reltuples for each uncovered table. Covered tables are absent from\n * the map — the caller keeps their real snapshot stats.\n */\nexport function synthesizeReltuples(\n  schema: FullSchema,\n  snapshot: ExportedStats[],\n): Map<string, number> {\n  const covered = new Map<string, number>();\n  for (const t of snapshot) {\n    covered.set(tableKey(t.schemaName, t.tableName), t.reltuples);\n  }\n\n  const edges = buildForeignKeyEdges(schema);\n  const parentOf = bestParentPerChild(edges, covered);\n  const globalFanout = medianCoveredFanout(edges, covered);\n  const dbMedian = median([...covered.values()]);\n\n  // Precompute, per parent, the covered children's fan-out ratios — the tier-1\n  // prior. Every FK edge counts (a table with two FKs is a child of both), and\n  // a child only counts when both it and its parent have real row counts.\n  const siblingFanouts = new Map<string, number[]>();\n  for (const { child, parent } of edges) {\n    const childRows = covered.get(child);\n    const parentRows = covered.get(parent);\n    if (\n      childRows === undefined ||\n      parentRows === undefined ||\n      parentRows === 0\n    ) {\n      continue;\n    }\n    const ratios = siblingFanouts.get(parent) ?? [];\n    ratios.push(childRows / parentRows);\n    siblingFanouts.set(parent, ratios);\n  }\n\n  const hasForeignKey = new Set(edges.map((e) => e.child));\n  const coveredMedianByClass = medianReltuplesByShapeClass(\n    schema,\n    covered,\n    hasForeignKey,\n  );\n\n  const synthesized = new Map<string, number>();\n  for (const table of schema.tables) {\n    const key = tableKey(table.schemaName, table.tableName);\n    if (covered.has(key)) continue;\n\n    synthesized.set(\n      key,\n      estimateRows(table, key, {\n        parentOf,\n        covered,\n        siblingFanouts,\n        globalFanout,\n        coveredMedianByClass,\n        hasForeignKey,\n        dbMedian,\n      }),\n    );\n  }\n  return synthesized;\n}\n\nexport interface ColumnStatHint {\n  /** Fraction of null values; 0 for a NOT NULL column. */\n  stanullfrac?: number;\n  /** pg_statistic stadistinct: negative is a fraction of rows, positive a count. */\n  stadistinct?: number;\n}\n\n/**\n * Infers coarse per-column planner stats for tables the snapshot doesn't cover,\n * from column type and constraints alone: a NOT NULL column has no nulls, a\n * unique or primary-key column is all-distinct, a boolean has two values. Keyed\n * by \"schema.table.column\"; a column with no inferable signal is absent, so the\n * caller keeps its default. This never fabricates a distribution (histogram or\n * MCV list) — only the two scalars the type reliably implies.\n */\nexport function inferColumnStats(\n  schema: FullSchema,\n): Map<string, ColumnStatHint> {\n  const uniqueColumns = new Set<string>();\n  for (const index of schema.indexes) {\n    if ((index.isUnique || index.isPrimary) && index.keyColumns.length === 1) {\n      uniqueColumns.add(\n        columnKey(index.schemaName, index.tableName, index.keyColumns[0].name),\n      );\n    }\n  }\n\n  const hints = new Map<string, ColumnStatHint>();\n  for (const table of schema.tables) {\n    for (const column of table.columns) {\n      if (column.dropped) continue;\n      const key = columnKey(table.schemaName, table.tableName, column.name);\n      const hint: ColumnStatHint = {};\n      if (!column.isNullable) hint.stanullfrac = 0;\n      if (uniqueColumns.has(key)) hint.stadistinct = -1;\n      else if (/^bool/i.test(column.columnType)) hint.stadistinct = 2;\n      if (hint.stanullfrac !== undefined || hint.stadistinct !== undefined) {\n        hints.set(key, hint);\n      }\n    }\n  }\n  return hints;\n}\n\nfunction columnKey(\n  schemaName: string | PgIdentifier,\n  tableName: string | PgIdentifier,\n  columnName: string | PgIdentifier,\n): string {\n  return `${tableKey(schemaName, tableName)}.${unquote(String(columnName))}`;\n}\n\ninterface EstimateContext {\n  parentOf: Map<string, string>;\n  covered: Map<string, number>;\n  siblingFanouts: Map<string, number[]>;\n  globalFanout: number | undefined;\n  coveredMedianByClass: Map<ShapeClass, number>;\n  hasForeignKey: Set<string>;\n  dbMedian: number | undefined;\n}\n\nfunction estimateRows(\n  table: FullSchemaTable,\n  key: string,\n  ctx: EstimateContext,\n): number {\n  const parentRows = anchoredParentRows(key, ctx.parentOf, ctx.covered);\n  if (parentRows !== undefined) {\n    const parent = ctx.parentOf.get(key)!;\n    // Tier 1: this parent's own covered children. Tier 2: the database-wide\n    // ratio. A parent with children but a degenerate 0-row count still falls\n    // through to the global prior.\n    const fanout =\n      median(ctx.siblingFanouts.get(parent) ?? []) ?? ctx.globalFanout;\n    if (fanout !== undefined) {\n      return Math.max(1, Math.round(parentRows * fanout));\n    }\n  }\n\n  // Tier between the FK ladder and the floor: a table with no usable FK anchor\n  // borrows the typical size of structurally similar covered tables — an event\n  // log sits near the other logs, a lookup near the other lookups.\n  const classMedian = ctx.coveredMedianByClass.get(\n    classifyShape(table, ctx.hasForeignKey.has(key)),\n  );\n  if (classMedian !== undefined) {\n    return Math.max(1, Math.round(classMedian));\n  }\n\n  // Floor: no anchor and no matching shape — the typical size of a table here.\n  return ctx.dbMedian ?? DEFAULT_SYNTHESIZED_ROWS;\n}\n\ntype ShapeClass = \"event\" | \"lookup\" | \"default\";\n\n/**\n * A coarse structural class for a table with no foreign key, used to borrow a\n * size from similar covered tables. \"event\" is an append-only log shape (a\n * timestamp column and more than one column); \"lookup\" is a small reference\n * table (a few columns, no timestamp). Everything else — including a table with\n * a foreign key, or one whose columns we don't have — is \"default\" and carries\n * no shape signal.\n */\nfunction classifyShape(\n  table: FullSchemaTable,\n  hasForeignKey: boolean,\n): ShapeClass {\n  if (hasForeignKey || table.columns.length === 0) return \"default\";\n  const hasTimestamp = table.columns.some((c) =>\n    /timestamp/i.test(c.columnType),\n  );\n  if (hasTimestamp && table.columns.length >= 2) return \"event\";\n  if (!hasTimestamp && table.columns.length <= 3) return \"lookup\";\n  return \"default\";\n}\n\n/** Median covered row count per shape class, for the \"event\" and \"lookup\" classes. */\nfunction medianReltuplesByShapeClass(\n  schema: FullSchema,\n  covered: Map<string, number>,\n  hasForeignKey: Set<string>,\n): Map<ShapeClass, number> {\n  const rowsByClass = new Map<ShapeClass, number[]>();\n  for (const table of schema.tables) {\n    const key = tableKey(table.schemaName, table.tableName);\n    const rows = covered.get(key);\n    if (rows === undefined) continue; // only covered tables inform a class\n    const cls = classifyShape(table, hasForeignKey.has(key));\n    if (cls === \"default\") continue;\n    const arr = rowsByClass.get(cls) ?? [];\n    arr.push(rows);\n    rowsByClass.set(cls, arr);\n  }\n\n  const medians = new Map<ShapeClass, number>();\n  for (const [cls, rows] of rowsByClass) {\n    const m = median(rows);\n    if (m !== undefined) medians.set(cls, m);\n  }\n  return medians;\n}\n\n/**\n * Walks the FK chain from `table` until it reaches a parent with a known row\n * count, so a new table pointing at another new table still anchors on real\n * data. Returns the covered ancestor's row count, or undefined if the whole\n * chain is uncovered (or cyclic).\n */\nfunction anchoredParentRows(\n  table: string,\n  parentOf: Map<string, string>,\n  covered: Map<string, number>,\n): number | undefined {\n  const seen = new Set<string>([table]);\n  let parent = parentOf.get(table);\n  while (parent !== undefined) {\n    const rows = covered.get(parent);\n    if (rows !== undefined) return rows;\n    if (seen.has(parent)) return undefined; // cycle\n    seen.add(parent);\n    parent = parentOf.get(parent);\n  }\n  return undefined;\n}\n\ninterface ForeignKeyEdge {\n  child: string;\n  parent: string;\n}\n\n/** Every foreign-key edge in the current schema, as child → parent keys. */\nfunction buildForeignKeyEdges(schema: FullSchema): ForeignKeyEdge[] {\n  const edges: ForeignKeyEdge[] = [];\n  for (const constraint of schema.constraints) {\n    if (constraint.constraintType !== \"foreign_key\") continue;\n    const parent = parseReferencedTable(constraint);\n    if (parent === undefined) continue;\n    edges.push({\n      child: tableKey(constraint.schemaName, constraint.tableName),\n      parent,\n    });\n  }\n  return edges;\n}\n\n/**\n * Picks one parent per child to anchor a new table's row estimate. When a table\n * has several foreign keys, prefer the covered parent with the most rows — it\n * bounds the child's cardinality most tightly and gives the best-grounded\n * estimate. When no parent is covered, keep the first (so the chain walk can\n * reach a covered ancestor). Fan-out priors still see every edge.\n */\nfunction bestParentPerChild(\n  edges: ForeignKeyEdge[],\n  covered: Map<string, number>,\n): Map<string, string> {\n  const parentsByChild = new Map<string, string[]>();\n  for (const { child, parent } of edges) {\n    const list = parentsByChild.get(child) ?? [];\n    list.push(parent);\n    parentsByChild.set(child, list);\n  }\n\n  const chosen = new Map<string, string>();\n  for (const [child, parents] of parentsByChild) {\n    let best = parents[0];\n    let bestRows = -1;\n    for (const parent of parents) {\n      const rows = covered.get(parent);\n      if (rows !== undefined && rows > bestRows) {\n        best = parent;\n        bestRows = rows;\n      }\n    }\n    chosen.set(child, best);\n  }\n  return chosen;\n}\n\n/** Parses the parent table out of a FK's `pg_get_constraintdef` text. */\nfunction parseReferencedTable(\n  constraint: FullSchemaConstraint,\n): string | undefined {\n  const match = constraint.definition.match(\n    /REFERENCES\\s+(?:(\"?[^\".\\s(]+\"?)\\.)?(\"?[^\".\\s(]+\"?)\\s*\\(/i,\n  );\n  if (!match) return undefined;\n  const schemaName = match[1]\n    ? unquote(match[1])\n    : unquote(String(constraint.schemaName));\n  const tableName = unquote(match[2]);\n  return `${schemaName}.${tableName}`;\n}\n\n/** Median child/parent ratio over every covered FK edge. */\nfunction medianCoveredFanout(\n  edges: ForeignKeyEdge[],\n  covered: Map<string, number>,\n): number | undefined {\n  const ratios: number[] = [];\n  for (const { child, parent } of edges) {\n    const childRows = covered.get(child);\n    const parentRows = covered.get(parent);\n    if (\n      childRows !== undefined &&\n      parentRows !== undefined &&\n      parentRows !== 0\n    ) {\n      ratios.push(childRows / parentRows);\n    }\n  }\n  return median(ratios);\n}\n\nfunction median(values: number[]): number | undefined {\n  if (values.length === 0) return undefined;\n  const sorted = [...values].sort((a, b) => a - b);\n  const mid = Math.floor(sorted.length / 2);\n  return sorted.length % 2 === 0\n    ? (sorted[mid - 1] + sorted[mid]) / 2\n    : sorted[mid];\n}\n\n// Snapshot rows (ExportedStats) carry plain-string identifiers; a FullSchema\n// carries PgIdentifier objects (a z.codec output). Coerce both to the same raw,\n// unquoted key so the two sides match.\nfunction tableKey(\n  schemaName: string | PgIdentifier,\n  tableName: string | PgIdentifier,\n): string {\n  return `${unquote(String(schemaName))}.${unquote(String(tableName))}`;\n}\n\nfunction unquote(identifier: string): string {\n  return identifier.startsWith('\"') && identifier.endsWith('\"')\n    ? identifier.slice(1, -1)\n    : identifier;\n}\n\n// Floor for a database whose snapshot is empty (no covered table to borrow a\n// size from). Rare — a snapshot with zero tables — but the estimate must still\n// be coherent rather than undefined.\nconst DEFAULT_SYNTHESIZED_ROWS = 10_000;\n"],"mappings":"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"}