Postgres's tablecmds.c carries the highest activity risk — 5 functions to address first

All five of the highest-risk functions in postgres/postgres sit in src/backend/commands/tablecmds.c, the ALTER TABLE implementation, and all five are classed as live-risk 'fire' functions.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk131.27Medium
Hottest FunctionATRewriteTable

Antipatterns Detected

neighbor_risk7exit_heavy5god_function5long_function5complex_branching4deeply_nested1

Run this on your own codebase

See if your own repo has a ATRewriteTable-style hotspot — run this in any local git repo:

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in postgres?

A god function is one that does too much: it is long, branches heavily and calls a large number of other functions, so understanding or changing it means holding many responsibilities in your head at once. In postgres, all five of the top hotspots in tablecmds.c carry this flag, with fan-out between 28 and 60 distinct callees. That breadth makes the functions hard to test in isolation and lets a change ripple into code that looks unrelated. It matters most here because the same file is where ALTER TABLE behavior is implemented, so these functions sit on paths many commands share.

How do I reduce cyclomatic complexity in c?

The main technique is extract-method: pull a self-contained branch or phase into a named function so the parent has fewer paths. Complexity above 15 deserves a look and above 30 deserves prompt attention. A concrete first step is in `ATExecChangeOwner`, where the switch over relation kinds could move into one small policy function, removing a large share of its 38 paths from the main body. For dispatch-style switches like `ATExecCmd`, the better goal is keeping each case a thin call rather than splitting the switch itself.

Is postgres actively maintained?

The data points to active development: all five top functions are in the fire quadrant, each with 1 touch in the last year and 12 days since last change, and 9,730 functions repo-wide fall in the fire quadrant. The file-level history for tablecmds.c shows 2 commits in the data, 2 distinct authors in the last 90 days, a bug-fix rate of 0% and no reverts. Active change and high structural complexity are not mutually exclusive, and here they coincide. I would read this as an actively edited module with concentrated complexity, not as a sign of neglect.

How do I reproduce this analysis?

Use the hotspots CLI from GitHub and check out commit 4848fd6 of postgres/postgres. Then run `hotspots analyze . --mode snapshot --explain-patterns --force`. The same command works on any local git repository without configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines structural complexity (cyclomatic complexity, nesting depth and fan-out) with how recently and how often a function has been changed. Functions that are both hard to understand and actively changing score highest, because that is where a new regression is most likely to be introduced. A very complex function that nobody has touched in years scores lower than a moderately complex one that changes every week. In this analysis the scores are tightly clustered, 130.0 to 131.27, because all five functions share the same recent change.

All 5 of the top hotspots in postgres/postgres live in one file, src/backend/commands/tablecmds.c, and each is in the ‘fire’ quadrant: structurally complex and changed 12 days before this snapshot. The highest-ranked, ATRewriteTable, has an activity-weighted risk score of 131.27, and the other four sit between 130.0 and 130.99, so no single function stands out and the whole module needs attention. This analysis covers 24,382 functions at commit 4848fd6, 4,562 of them in the critical band. I would start with ATRewriteTable, which has the highest fan-out of the five (60), and keep ATExecCmd, which has the highest complexity (76), as a thin dispatch switch.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
ATRewriteTablesrc/backend/commands/tablecmds.c131.341460
ATExecChangeOwnersrc/backend/commands/tablecmds.c131.038449
MergeConstraintsIntoExistingsrc/backend/commands/tablecmds.c130.826528
ATExecCmdsrc/backend/commands/tablecmds.c130.276254
RemoveInheritancesrc/backend/commands/tablecmds.c130.023435

One file, five hotspots

When the top five all share a file, the usual advice of ‘refactor the worst function’ stops being useful. Anyone touching ALTER TABLE behavior in tablecmds.c is likely to land in one of these functions, or in a neighbor that calls into them. The repository-wide picture is below: 9,730 functions are in the fire quadrant and 14,652 are in watch. None are classed as debt or ok.

Triage Band Distribution
Fire9730Watch14652

24,382 functions analyzed

The antipattern mix across the five is consistent. Every one of them is flagged as a god function, long function and exit-heavy, and neighbor risk shows up in all five. It is also flagged on 2 of the 5 context-only functions, 7 occurrences in total.

Detected Antipatterns
Neighbor Risk×7Neighbor Risk
Co-located with other high-risk functions in the same file, compounding the blast radius of any change to that file.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Complex Branching×4Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×1Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.

Two caveats before the function-level analysis. The activity-weighted scores sit within about one point of each other, and every function shows 1 touch in the last year with 12 days since last change. That points to one recent change to the file rather than five independent churn histories. The historical signals for the file are also mild: 2 commits total, a bug-fix rate of 0%, no reverts, no bug-linked commits and no review comments. So I would frame this as concentrated structural complexity in actively edited code, not as evidence of a defect-prone module.

ATRewriteTable — tablecmds.c

ATRewriteTable
src/backend/commands/tablecmds.c
131.27
fire
CC 41
ND 4
FO 60
touches/yr 1

From the name and the excerpt, this is the routine that rewrites a table’s contents, opening the old relation and optionally a new heap, and preparing bulk-insert state and executor state along the way. The excerpt also shows it building expression states for constraints and partition-check quals. A cyclomatic complexity of 41 is high, but the number that stands out is fan-out: 60 distinct callees is the highest of the five, so the function touches a very wide slice of the backend. That is the main blast-radius concern for any change here.

The visible structure suggests several distinct phases in one body: relation setup, insert-state setup, executor and constraint preparation, and presumably the row-processing loop. I would start here and extract the setup phases into named helpers so the core rewrite loop can be reviewed on its own. With a nesting depth of only 4, extraction should be mechanical rather than a restructuring exercise.

ATExecChangeOwner — tablecmds.c

ATExecChangeOwner
src/backend/commands/tablecmds.c
130.99
fire
CC 38
ND 4
FO 49
touches/yr 1

This handles ALTER … OWNER. The excerpt shows a switch over relation kinds that decides whether ownership can change: tables, views, materialized views and foreign tables are allowed, indexes are handled with a warning, partitioned indexes error out unless the call is recursive, and sequences have their own checks. Much of the complexity of 38 appears to come from that per-kind policy, with a fan-out of 49 on top.

The relkind policy is the natural seam. A small function that answers ‘can this relation kind change owner, and how’ would pull the switch and its messages out of the main flow and leave the ownership update easier to follow. Because the function is exit-heavy, each early-out path is a distinct test case, so I would write one test per relkind branch before moving anything.

MergeConstraintsIntoExisting — tablecmds.c

MergeConstraintsIntoExisting
src/backend/commands/tablecmds.c
130.79
fire
CC 26
ND 5
FO 28
touches/yr 1

This is the only function in the set flagged as deeply nested, with a nesting depth of 5, the deepest of the five. The excerpt shows an outer scan over the parent’s constraints with an inner scan over the child’s, matching CHECK constraints by name and NOT NULL constraints by attribute number. A nested catalog-scan loop with different matching rules per constraint type is where depth and branching pile up.

I would extract the inner child-matching logic into a function that returns the matching child constraint, if any. That should reduce the nesting and lets the CHECK and NOT NULL matching rules be tested separately. The complexity of 26 is the second-lowest of the five, behind RemoveInheritance at 23, and the fan-out of 28 is the lowest, so this is probably among the cheaper ones to improve.

ATExecCmd — tablecmds.c

ATExecCmd
src/backend/commands/tablecmds.c
130.19
fire
CC 76
ND 2
FO 54
touches/yr 1

Here the cyclomatic complexity is the highest in the set at 76, yet the nesting depth is only 2. The excerpt explains why: it is a large switch over ALTER TABLE subcommand types, with most cases dispatching to a dedicated routine such as the add-column, drop-column, set-statistics and add-index handlers, then breaking. That is a dispatch table written as a switch, which is easier to reason about than the same complexity expressed through nested conditionals.

Still, 76 paths with a fan-out of 54 means every new ALTER TABLE subcommand adds another case here, and every subcommand passes through this one function. I would not rush to split it. A first step would be to confirm that each case stays a thin call, and to move the cases that do more than dispatch, such as those that transform the command first, into their own handlers so the switch stays uniform.

RemoveInheritance — tablecmds.c

RemoveInheritance
src/backend/commands/tablecmds.c
130
fire
CC 23
ND 4
FO 35
touches/yr 1

This detaches a child relation from a parent. The excerpt shows it deleting the inheritance record, erroring differently for partitions and for ordinary inheritance when the link is missing, then scanning the child’s attributes to decrement inheritance counts, with further handling for check and not-null constraints afterwards. Its complexity of 23 is the lowest of the five, but it carries a fan-out of 35 and a nesting depth of 4.

It appears to handle three separate catalog concerns in sequence: the inheritance row, the attributes and the constraints. Each is a candidate for its own helper, which would leave the top-level function reading as three named steps. The partition-versus-inheritance error handling is a small decomposition on its own.

What the neighbors tell me

The other functions in the same file that the analysis surfaces as context are low in structural complexity, with complexity values between 3 and 6. Their scores are likely elevated by the same recent activity (1 touch, 12 days since last change) seen across the file. They are in the watch quadrant, not refactoring priorities. The neighbor-risk flag on the top five is the more useful signal: changes in these functions may need coordinated edits elsewhere, and every function listed shows the same 12 days since last change.

Where I would put effort

  1. Start with ATRewriteTable: highest fan-out (60) and highest score.
  2. Add per-branch tests to ATExecChangeOwner before touching its relkind switch.
  3. Flatten the nested scans in MergeConstraintsIntoExisting.
  4. Leave ATExecCmd as a switch, but keep each case a thin call.
  5. Split RemoveInheritance into its three catalog steps.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
neighbor_risk7
exit_heavy5
god_function5
long_function5
complex_branching4
deeply_nested1

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function, neighbor_risk.

Reproduce This Analysis

git clone https://github.com/postgres/postgres
cd postgres
git checkout 4848fd6752ad6b88c5a066442df35ba208dfcec5
hotspots analyze . --mode snapshot --explain-patterns --force

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Was this useful? Let me know →

Related Analyses