java-topology/tests/support/PostgresqlJoinMergeAlgorithm.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

194 lines
8.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package support;
import java.util.*;
/**
* PostgresqlJoinMergeAlgorithm — models analyzejoins.c site postgresql-0004.
*
* DEFECT (CWE-407): When eliminating a redundant join (remove_useless_joins),
* PostgreSQL transfers target-list expressions from the eliminated relation
* (toRemove) to the surviving relation (toKeep). For each expression in
* toRemove's reltarget, it calls list_member() to check if an equivalent
* expression already exists in toKeep's reltarget. list_member() uses
* structural equal(), making the membership check O(|toKeep.exprs|) per node.
* With N expressions in toRemove and M in toKeep, the merge is O(N × M).
*
* Site in PostgreSQL source:
* analyzejoins.c:1914 — remove_self_join_rel(), reltarget merge loop
*
* Defective C pattern:
*
* foreach(lc, toRemove->reltarget->exprs) {
* Node *node = lfirst(lc);
* ChangeVarNodesExtended(node, toRemove->relid, toKeep->relid, 0, ...);
* if (!list_member(toKeep->reltarget->exprs, node)) // O(M) — DEFECT
* toKeep->reltarget->exprs = lappend(..., node);
* }
*
* Fix: Build a HashSet<Expr> from toKeep's exprs before the loop.
* Each list_member() call drops from O(M) to O(1). Total: O(N + M).
*
* C note: Requires structural equality (i.e., equal()) for correctness,
* because after ChangeVarNodesExtended both toRemove's and toKeep's Var
* nodes reference toKeep->relid — they may be semantically identical but
* are distinct allocations. Pointer identity (list_member_ptr) would be
* wrong and would insert duplicates. Full fix requires nodeHash() for a
* C hash set, or a Bitmapset for the Var-only case.
*
* Comparison counts (N nodes in toRemove.exprs, M in toKeep.exprs):
* defective: N × M (each toRemove node scans all M toKeep nodes)
* fixed: M + N (M to build HashSet, then N O(1) lookups)
*
* Growth when N=M=k doubles: defective ≈4× (quadratic), fixed ≈2× (linear).
*/
public class PostgresqlJoinMergeAlgorithm {
// ── Expr: models a rewritten target-list expression ──────────────────────
//
// After ChangeVarNodesExtended, an expression from toRemove has its
// relation reference updated to toKeep's relid. Two expressions are
// equal() if they reference the same column (same varattno after rewrite).
// We model this as a simple integer ID.
public static final class Expr {
/** Canonical ID after relid rewrite — models varattno post-rewrite. */
public final int id;
public Expr(int id) { this.id = id; }
/** Structural equality — models PostgreSQL equal() post-rewrite. */
@Override
public boolean equals(Object o) {
return o instanceof Expr e && id == e.id;
}
@Override
public int hashCode() { return Integer.hashCode(id); }
@Override
public String toString() { return "Expr(" + id + ")"; }
}
// ── RelTarget: models PathTarget (toRemove / toKeep reltarget) ───────────
public static final class RelTarget {
public final List<Expr> exprs;
public RelTarget(List<Expr> exprs) {
this.exprs = new ArrayList<>(exprs);
}
}
// ── Result ────────────────────────────────────────────────────────────────
public static final class Result {
/** toKeep's exprs after merge. */
public final List<Expr> mergedExprs;
/** Total element-level equality checks performed. */
public final long comparisons;
public Result(List<Expr> mergedExprs, long comparisons) {
this.mergedExprs = mergedExprs;
this.comparisons = comparisons;
}
}
// ── Defective: list_member scan — O(M) per node ──────────────────────────
//
// Models the BEFORE state. ChangeVarNodesExtended is modeled as a no-op
// (the rewrite is already baked into Expr.id — both sides use the same
// canonical ID space post-rewrite).
public static Result defectiveMerge(RelTarget toRemove, RelTarget toKeep) {
List<Expr> keepExprs = new ArrayList<>(toKeep.exprs);
long comparisons = 0;
for (Expr node : toRemove.exprs) {
// list_member: scan keepExprs calling equal() on each element
boolean found = false;
for (Expr existing : keepExprs) {
comparisons++;
if (existing.equals(node)) {
found = true;
break;
}
}
if (!found)
keepExprs.add(node);
}
return new Result(keepExprs, comparisons);
}
// ── Fixed: HashSet<Expr> from toKeep — O(1) per node ─────────────────────
//
// Build a HashSet from toKeep's exprs before the loop.
// C equivalent: nodeHash() needed for general expressions; Bitmapset
// sufficient for pure Var nodes (no nodeHash() required for that subset).
public static Result fixedMerge(RelTarget toRemove, RelTarget toKeep) {
List<Expr> keepExprs = new ArrayList<>(toKeep.exprs);
Set<Expr> keepSet = new HashSet<>(toKeep.exprs);
long comparisons = toKeep.exprs.size(); // cost of building HashSet
for (Expr node : toRemove.exprs) {
comparisons++; // O(1) hash lookup
if (!keepSet.contains(node)) {
keepExprs.add(node);
keepSet.add(node);
}
}
return new Result(keepExprs, comparisons);
}
// ── Self-test ─────────────────────────────────────────────────────────────
public static void main(String[] args) {
System.out.println("PostgresqlJoinMergeAlgorithm — postgresql-0004 (analyzejoins.c:1914)");
System.out.println("Defect: list_member O(M) scan when merging reltarget expr lists");
System.out.println("Fix: HashSet<Expr> built once from toKeep — O(1) per node");
System.out.println();
System.out.println("NOTE: C fix requires nodeHash() for full O(1) correctness.");
System.out.println(" Bitmapset covers the Var-only subset without nodeHash().");
System.out.println();
int[] sizes = {10, 25, 50, 100, 200, 500};
System.out.printf("%-12s %-16s %-12s %s%n",
"N=M exprs", "Defective ops", "Fixed ops", "Speedup");
System.out.println("".repeat(56));
Random rng = new Random(42);
for (int n : sizes) {
// toKeep has M exprs (IDs 0..M-1)
// toRemove has N exprs: half overlap with toKeep, half new
int overlap = n / 2;
List<Expr> keepExprs = new ArrayList<>();
for (int i = 0; i < n; i++) keepExprs.add(new Expr(i));
List<Expr> removeExprs = new ArrayList<>();
for (int i = 0; i < overlap; i++) removeExprs.add(new Expr(i)); // duplicates
for (int i = n; i < n + (n - overlap); i++) removeExprs.add(new Expr(i)); // new
Collections.shuffle(removeExprs, rng);
RelTarget toKeep = new RelTarget(keepExprs);
RelTarget toRemove = new RelTarget(removeExprs);
Result def = defectiveMerge(toRemove, toKeep);
Result fix = fixedMerge(toRemove, toKeep);
assert def.mergedExprs.size() == fix.mergedExprs.size()
: "merge size mismatch at N=" + n;
System.out.printf("%-12d %-16d %-12d %.1fx%n",
n,
def.comparisons,
fix.comparisons,
(double) def.comparisons / Math.max(1, fix.comparisons));
}
System.out.println();
System.out.println("Defective: O(N × M) — each toRemove node scans all toKeep nodes.");
System.out.println("Fixed: O(N + M) — build once, then O(1) lookups.");
}
}