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.
224 lines
8.6 KiB
Java
224 lines
8.6 KiB
Java
package support;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* PostgresqlExprMembershipAlgorithm — models equivclass.c site postgresql-0003.
|
||
*
|
||
* DEFECT (CWE-407): list_member() is called inside a nested loop to check
|
||
* whether each Var pulled from an equivalence-class member expression is
|
||
* present in a pre-built list (exprvars). The outer list (exprvars) is fixed
|
||
* for the duration of the scan, but rebuilt from scratch via list_member()
|
||
* on every inner-loop iteration — O(|exprvars|) per emvar, O(|exprvars| ×
|
||
* sum(|emvars|)) total.
|
||
*
|
||
* Site in PostgreSQL source:
|
||
* equivclass.c:1041 — find_em_expr_for_rel(), inner membership check
|
||
*
|
||
* Defective C pattern:
|
||
*
|
||
* exprvars = pull_var_clause(some_expr, ...); // fixed for this call
|
||
* foreach(lc, ec->ec_members) {
|
||
* EquivalenceMember *em = ...;
|
||
* emvars = pull_var_clause(em->em_expr, ...); // varies per member
|
||
* foreach(lc2, emvars) {
|
||
* if (!list_member(exprvars, lfirst(lc2))) // O(|exprvars|) — DEFECT
|
||
* break;
|
||
* }
|
||
* }
|
||
*
|
||
* Fix: Convert exprvars to a HashSet<Expr> once before the outer loop.
|
||
* Each list_member() call becomes O(1). Total cost drops from
|
||
* O(|exprvars| × sum(|emvars|)) to O(|exprvars| + sum(|emvars|)).
|
||
*
|
||
* C equivalent: If emvars are pure Var nodes (common case), use a
|
||
* Bitmapset keyed on (varno * 3200 + varattno + 1600) — no nodeHash()
|
||
* needed. For non-Var nodes, fall back to list_member().
|
||
*
|
||
* Comparison counts (M ec_members, each with K emvars, exprvars size E):
|
||
* defective: M × K × E (K checks per member, each scanning E exprvars)
|
||
* fixed: E + M × K (E to build HashSet, then 1 per emvar check)
|
||
*
|
||
* Growth when E doubles: defective scales linearly with E, fixed does not.
|
||
*/
|
||
public class PostgresqlExprMembershipAlgorithm {
|
||
|
||
// ── Expr: models a PostgreSQL expression node ─────────────────────────────
|
||
//
|
||
// In real PostgreSQL, expressions are tree nodes compared via equal().
|
||
// We model them as integer IDs for clarity; equals()/hashCode() are
|
||
// based on the ID, analogous to structural equal() on two Var nodes
|
||
// with the same (varno, varattno, varlevelsup).
|
||
|
||
public static final class Expr {
|
||
public final int id; // unique expression identity (encodes Var fields)
|
||
|
||
public Expr(int id) { this.id = id; }
|
||
|
||
/** Structural equality — models PostgreSQL equal() for Var nodes. */
|
||
@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 + ")"; }
|
||
}
|
||
|
||
// ── EquivalenceMember: models EquivalenceMember with em_expr ─────────────
|
||
|
||
public static final class EquivalenceMember {
|
||
public final Expr emExpr;
|
||
public final List<Expr> emVars; // pull_var_clause(em_expr) result
|
||
|
||
public EquivalenceMember(Expr emExpr, List<Expr> emVars) {
|
||
this.emExpr = emExpr;
|
||
this.emVars = emVars;
|
||
}
|
||
}
|
||
|
||
// ── Result ────────────────────────────────────────────────────────────────
|
||
|
||
public static final class Result {
|
||
/** The first EquivalenceMember whose emvars are all present in exprvars. */
|
||
public final EquivalenceMember found;
|
||
/** Total element-level equality checks performed. */
|
||
public final long comparisons;
|
||
|
||
public Result(EquivalenceMember found, long comparisons) {
|
||
this.found = found;
|
||
this.comparisons = comparisons;
|
||
}
|
||
}
|
||
|
||
// ── Defective: list_member scan — O(|exprvars|) per emvar ────────────────
|
||
|
||
public static Result defectiveFindEm(
|
||
List<Expr> exprvars,
|
||
List<EquivalenceMember> ecMembers) {
|
||
|
||
long comparisons = 0;
|
||
|
||
for (EquivalenceMember em : ecMembers) {
|
||
boolean allPresent = true;
|
||
|
||
for (Expr emvar : em.emVars) {
|
||
// list_member: scan exprvars calling equal() on each element
|
||
boolean found = false;
|
||
for (Expr ev : exprvars) {
|
||
comparisons++;
|
||
if (ev.equals(emvar)) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
allPresent = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (allPresent)
|
||
return new Result(em, comparisons);
|
||
}
|
||
|
||
return new Result(null, comparisons);
|
||
}
|
||
|
||
// ── Fixed: HashSet<Expr> from exprvars — O(1) per emvar ─────────────────
|
||
//
|
||
// Build a HashSet from exprvars once before the outer loop.
|
||
// C equivalent: Bitmapset for Var nodes; fall back to list_member for
|
||
// non-Var nodes (PlaceHolderVar, Aggref, etc.).
|
||
|
||
public static Result fixedFindEm(
|
||
List<Expr> exprvars,
|
||
List<EquivalenceMember> ecMembers) {
|
||
|
||
// Build HashSet once — O(|exprvars|)
|
||
Set<Expr> exprSet = new HashSet<>(exprvars);
|
||
long comparisons = exprvars.size(); // cost of building the set
|
||
|
||
for (EquivalenceMember em : ecMembers) {
|
||
boolean allPresent = true;
|
||
|
||
for (Expr emvar : em.emVars) {
|
||
comparisons++; // O(1) hash lookup
|
||
if (!exprSet.contains(emvar)) {
|
||
allPresent = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (allPresent)
|
||
return new Result(em, comparisons);
|
||
}
|
||
|
||
return new Result(null, comparisons);
|
||
}
|
||
|
||
// ── Self-test ─────────────────────────────────────────────────────────────
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("PostgresqlExprMembershipAlgorithm — postgresql-0003 (equivclass.c:1041)");
|
||
System.out.println("Defect: list_member O(n) scan inside nested EC member loop");
|
||
System.out.println("Fix: HashSet<Expr> built once from exprvars — O(1) per check");
|
||
System.out.println();
|
||
|
||
int[] exprvarSizes = {10, 50, 100, 500, 1000};
|
||
int emMembers = 20, emVarsPerMember = 5;
|
||
|
||
System.out.printf("%-12s %-16s %-12s %s%n",
|
||
"|exprvars|", "Defective ops", "Fixed ops", "Speedup");
|
||
System.out.println("─".repeat(56));
|
||
|
||
Random rng = new Random(42);
|
||
for (int exprSize : exprvarSizes) {
|
||
List<Expr> exprvars = buildExprList(exprSize, 0);
|
||
List<EquivalenceMember> members = buildEcMembers(
|
||
emMembers, emVarsPerMember, exprSize, rng);
|
||
|
||
Result def = defectiveFindEm(exprvars, members);
|
||
Result fix = fixedFindEm(exprvars, members);
|
||
|
||
assert Objects.equals(
|
||
def.found != null ? def.found.emExpr : null,
|
||
fix.found != null ? fix.found.emExpr : null)
|
||
: "result mismatch at |exprvars|=" + exprSize;
|
||
|
||
System.out.printf("%-12d %-16d %-12d %.1fx%n",
|
||
exprSize,
|
||
def.comparisons,
|
||
fix.comparisons,
|
||
(double) def.comparisons / Math.max(1, fix.comparisons));
|
||
}
|
||
|
||
System.out.println();
|
||
System.out.println("Defective: O(|exprvars| × M × K) — scales with exprvars size.");
|
||
System.out.println("Fixed: O(|exprvars| + M × K) — exprvars built once.");
|
||
}
|
||
|
||
public static List<Expr> buildExprList(int size, int offset) {
|
||
List<Expr> list = new ArrayList<>(size);
|
||
for (int i = 0; i < size; i++) list.add(new Expr(offset + i));
|
||
return list;
|
||
}
|
||
|
||
public static List<EquivalenceMember> buildEcMembers(
|
||
int count, int emVarsEach, int exprSize, Random rng) {
|
||
List<EquivalenceMember> members = new ArrayList<>();
|
||
for (int i = 0; i < count; i++) {
|
||
List<Expr> emVars = new ArrayList<>();
|
||
for (int j = 0; j < emVarsEach; j++) {
|
||
// Half the time pick from exprvars (found), half outside (not found)
|
||
int id = rng.nextBoolean() ? rng.nextInt(exprSize) : exprSize + rng.nextInt(10);
|
||
emVars.add(new Expr(id));
|
||
}
|
||
members.add(new EquivalenceMember(new Expr(exprSize + 100 + i), emVars));
|
||
}
|
||
return members;
|
||
}
|
||
}
|