package support; import java.util.*; /** * PostgresqlVarDedupAlgorithm — models preptlist.c sites postgresql-0002. * * DEFECT (CWE-407): tlist_member() walks the entire target list calling * structural equal() on each entry to check if a Var is already present. * As the list grows, each check takes longer. Building a tlist of N unique * Vars from a pool of M candidates costs O(M × N) — quadratic when M ≈ N. * * Sites in PostgreSQL source (preptlist.c): * :180 — MERGE action targetList/qual Var collection * :206 — mergeJoinCondition Var collection * :316 — RETURNING clause Var collection * * Defective C pattern (all three sites identical): * * vars = pull_var_clause(..., PVC_INCLUDE_PLACEHOLDERS); * foreach(l, vars) { * Var *var = (Var *) lfirst(l); * if (IsA(var, Var) && var->varno == result_relation) * continue; // skip target-rel vars * if (tlist_member((Expr *) var, tlist)) // O(n) — THE DEFECT * continue; * tlist = lappend(tlist, makeTargetEntry(...)); * } * * Fix (Path B — no nodeHash() required): * Maintain a HashSet (C: Bitmapset keyed on encoded varno×varattno) * alongside the tlist. Check/update the set instead of scanning tlist. * Reduces per-var check from O(n) to O(1). Total: O(n) instead of O(n²). * * C encoding: bms_member(varno * 3200 + varattno + 1600) * Safe for varno ≤ 65001 (INNER_VAR) and varattno in [-1600, 1600]. * * Comparison counts: * defective: 0+1+2+...+(N-1) = N*(N-1)/2 (N unique vars, each checked * against all previously added entries) * fixed: N (one O(1) hash check per var) * * Growth when N doubles: defective ≈4× (quadratic), fixed ≈2× (linear). */ public class PostgresqlVarDedupAlgorithm { // ── Var: models PostgreSQL Var node ────────────────────────────────────── // // A Var represents a column reference: (varno=relation, varattno=column, // varlevelsup=query nesting depth). PostgreSQL equal() on Vars compares // all three fields plus vartype and several others. We model the minimum // set needed to reproduce the defect: varno + varattno + varlevelsup. public static final class Var { public final int varno; // relation index (1-based) public final int varattno; // column number (negative = system col) public final int varlevelsup; // query nesting (0 = current query) public Var(int varno, int varattno, int varlevelsup) { this.varno = varno; this.varattno = varattno; this.varlevelsup = varlevelsup; } /** Structural equality — models PostgreSQL equal() for Var nodes. */ @Override public boolean equals(Object o) { if (!(o instanceof Var v)) return false; return varno == v.varno && varattno == v.varattno && varlevelsup == v.varlevelsup; } @Override public int hashCode() { return Objects.hash(varno, varattno, varlevelsup); } @Override public String toString() { return "Var(" + varno + "," + varattno + "," + varlevelsup + ")"; } } // ── TargetEntry: wraps an expression in the target list ────────────────── public static final class TargetEntry { public final Var expr; public final int resno; // position in tlist (1-based) public TargetEntry(Var expr, int resno) { this.expr = expr; this.resno = resno; } } // ── Result ──────────────────────────────────────────────────────────────── public static final class Result { public final List tlist; /** Number of element-level equality checks performed. */ public final long comparisons; public Result(List tlist, long comparisons) { this.tlist = tlist; this.comparisons = comparisons; } } // ── Defective: tlist_member scan — O(n) per var ────────────────────────── // // Exact port of the defective PostgreSQL pattern. // Each call to tlist_member() walks the existing tlist, calling equal() // on each entry's expr until it finds a match or exhausts the list. public static Result defectiveBuildTlist(List vars, int resultRelation) { List tlist = new ArrayList<>(); long comparisons = 0; for (Var var : vars) { if (var.varno == resultRelation) continue; // skip: belongs to target relation // tlist_member: scan tlist, call equal() on each expr boolean found = false; for (TargetEntry te : tlist) { comparisons++; if (te.expr.equals(var)) { found = true; break; } } if (found) continue; tlist.add(new TargetEntry(var, tlist.size() + 1)); } return new Result(tlist, comparisons); } // ── Fixed: HashSet on Var identity — O(1) per var ──────────────────────── // // Maintain a HashSet alongside the tlist. // C equivalent: Bitmapset keyed on (varno * 3200 + varattno + 1600). // Does NOT require nodeHash() — Var identity is three plain integers. public static Result fixedBuildTlist(List vars, int resultRelation) { List tlist = new ArrayList<>(); Set seen = new HashSet<>(); long comparisons = 0; for (Var var : vars) { if (var.varno == resultRelation) continue; comparisons++; // one O(1) hash table lookup if (seen.contains(var)) continue; tlist.add(new TargetEntry(var, tlist.size() + 1)); seen.add(var); } return new Result(tlist, comparisons); } // ── Self-test ───────────────────────────────────────────────────────────── public static void main(String[] args) { System.out.println("PostgresqlVarDedupAlgorithm — postgresql-0002 (preptlist.c)"); System.out.println("Defect: tlist_member O(n) check per var while building junk tlist"); System.out.println("Fix: HashSet / Bitmapset — O(1) per check"); System.out.println(); // Simulate a MERGE with multiple actions referencing many columns. // resultRelation=1, source table=2, many columns referenced. int[] sizes = {10, 20, 50, 100, 200, 500}; System.out.printf("%-8s %-14s %-10s %-10s %s%n", "N vars", "Defective ops", "Fixed ops", "Speedup", "Tlist size"); System.out.println("─".repeat(62)); for (int n : sizes) { List vars = buildVarPool(n, /*resultRelation=*/1, /*duplicates=*/n / 2); Result def = defectiveBuildTlist(vars, 1); Result fix = fixedBuildTlist(vars, 1); // Sanity: both versions must produce the same tlist assert def.tlist.size() == fix.tlist.size() : "tlist size mismatch at N=" + n; System.out.printf("%-8d %-14d %-10d %-10.1fx %d%n", vars.size(), def.comparisons, fix.comparisons, (double) def.comparisons / Math.max(1, fix.comparisons), def.tlist.size()); } System.out.println(); System.out.println("Exact defective count = N*(N-1)/2 (triangular number)."); System.out.println("Fixed count = N (one hash lookup per unique var)."); } /** Builds a pool of vars mixing unique entries and duplicates. */ public static List buildVarPool(int uniqueVars, int resultRelation, int extraDuplicates) { List pool = new ArrayList<>(); // unique vars from relation 2 for (int att = 1; att <= uniqueVars; att++) { pool.add(new Var(2, att, 0)); } // duplicates (already in pool — trigger the O(n) scan) Random rng = new Random(42); for (int i = 0; i < extraDuplicates; i++) { int att = rng.nextInt(uniqueVars) + 1; pool.add(new Var(2, att, 0)); } // result relation vars (filtered out by both versions) for (int att = 1; att <= 5; att++) { pool.add(new Var(resultRelation, att, 0)); } Collections.shuffle(pool, rng); return pool; } }