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 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 exprs; public RelTarget(List exprs) { this.exprs = new ArrayList<>(exprs); } } // ── Result ──────────────────────────────────────────────────────────────── public static final class Result { /** toKeep's exprs after merge. */ public final List mergedExprs; /** Total element-level equality checks performed. */ public final long comparisons; public Result(List 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 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 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 keepExprs = new ArrayList<>(toKeep.exprs); Set 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 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 keepExprs = new ArrayList<>(); for (int i = 0; i < n; i++) keepExprs.add(new Expr(i)); List 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."); } }