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.
194 lines
8.1 KiB
Java
194 lines
8.1 KiB
Java
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.");
|
||
}
|
||
}
|