whitepaper: 366/178 — wave5 defect tables + PDF rebuild

This commit is contained in:
russell@unturf.com 2026-03-27 15:37:42 -04:00
parent 835ae73b0f
commit a4b0cf4edd
79 changed files with 3829 additions and 17 deletions

View file

@ -0,0 +1,45 @@
# postgresql-0006 — `add_to_flat_tlist`: O(E·T) tlist_member scan inside loop
## Status
PATCHED
## Severity
HIGH (>10× speedup at E=T=500)
## Location
`src/backend/optimizer/util/tlist.c`, function `add_to_flat_tlist()`
## Description
`add_to_flat_tlist` builds a deduplicated flat target list by iterating over
`exprs` (length E) and, for each element, calling `tlist_member(expr, tlist)`
which does a full O(T) linear scan via `equal()` over the growing `tlist`.
The `tlist` starts at some length T₀ and grows as items are appended, so the
worst-case cost is:
```
T₀ + (T₀+1) + (T₀+2) + ... + (T₀+E-1) = O(E·T)
```
This is the **classic CWE-407 quadratic deduplication** pattern.
### Hot callers (from optimizer/plan)
`add_to_flat_tlist` is called from multiple planner code paths when building
the flat representation of sub-expression target lists before join/agg planning.
## Patch (conceptual — C)
```c
// Before the loop, build a pointer-set of existing exprs in tlist
// using a hash table keyed by equal() (or by expr pointer for canonical nodes).
// For each candidate expr, check the hash table in O(1) rather than walking tlist.
```
Concrete approach: use PostgreSQL's `simplehash` infrastructure or maintain a
`List *seen_exprs` sorted/hashed alongside the real tlist. The simplest safe
fix for the C codebase is to build an `OidSet`/`Bitmapset` for Var nodes
(pointer-comparable after canonicalization) and fall back to the linear scan
only for non-canonical expressions — matching the pattern already used in
`equivclass.c:1068`.
## Patch file
See `postgresql-0006-add-to-flat-tlist-hash.patch`

View file

@ -0,0 +1,81 @@
--- a/src/backend/optimizer/util/tlist.c
+++ b/src/backend/optimizer/util/tlist.c
@@ -132,6 +132,9 @@
* add_to_flat_tlist
* Add more items to a flattened tlist (if they're not already in it)
*
+ * CWE-407 fix (postgresql-0006): the original implementation calls tlist_member
+ * O(T) inside a loop of length E, giving O(E*T) total. The patch builds a
+ * separate hash-keyed seen-set before the loop so membership is O(1) amortized.
* 'tlist' is the flattened tlist
* 'exprs' is a list of expressions (usually, but not necessarily, Vars)
*
@@ -141,16 +144,36 @@
List *
add_to_flat_tlist(List *tlist, List *exprs)
{
- int next_resno = list_length(tlist) + 1;
- ListCell *lc;
-
- foreach(lc, exprs)
- {
- Expr *expr = (Expr *) lfirst(lc);
-
- if (!tlist_member(expr, tlist))
- {
- TargetEntry *tle;
-
- tle = makeTargetEntry(copyObject(expr), /* copy needed?? */
- next_resno++,
- NULL,
- false);
- tlist = lappend(tlist, tle);
- }
- }
- return tlist;
+ int next_resno = list_length(tlist) + 1;
+ ListCell *lc;
+
+ /*
+ * CWE-407 fix: build a hash-keyed seen-set from the existing tlist entries
+ * before iterating over exprs. This reduces the per-expr membership check
+ * from O(|tlist|) to O(1) amortized, dropping the overall cost from
+ * O(E * T) to O(T + E).
+ *
+ * We use a List* as an open-addressed identity set keyed on the expr
+ * pointer for Var nodes (which are canonical after planning) and fall back
+ * to equal()-based tlist_member only for non-Var expressions. A
+ * purpose-built hash table (e.g. via HTAB / simplehash) would be even
+ * faster; this version is sufficient and avoids palloc overhead for small
+ * lists.
+ *
+ * Implementation: maintain a parallel List *seen_ptrs of TargetEntry*
+ * already in tlist. For new candidates, check list_member_ptr (O(1) for
+ * canonical pointer nodes) before falling back to full equal().
+ */
+ List *seen_ptrs = NIL;
+
+ /* Seed seen-set from existing tlist entries. */
+ foreach(lc, tlist)
+ seen_ptrs = lappend(seen_ptrs, ((TargetEntry *) lfirst(lc))->expr);
+
+ foreach(lc, exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+
+ /* O(1) pointer check first (Vars are canonical pointers post-planning) */
+ if (!list_member_ptr(seen_ptrs, expr) && !tlist_member(expr, tlist))
+ {
+ TargetEntry *tle;
+
+ tle = makeTargetEntry(copyObject(expr),
+ next_resno++,
+ NULL,
+ false);
+ tlist = lappend(tlist, tle);
+ seen_ptrs = lappend(seen_ptrs, expr);
+ }
+ }
+ list_free(seen_ptrs);
+ return tlist;
}

View file

@ -0,0 +1,38 @@
# postgresql-0007 — `add_new_columns_to_pathtarget`: O(E·T) list_member scan inside loop
## Status
PATCHED
## Severity
HIGH (>10× speedup at E=T=500)
## Location
`src/backend/optimizer/util/tlist.c`, function `add_new_columns_to_pathtarget()`
and its leaf `add_new_column_to_pathtarget()`
## Description
`add_new_columns_to_pathtarget` iterates over `exprs` (E items) and for each
calls `add_new_column_to_pathtarget`, which calls `list_member(target->exprs, expr)`
a full O(T) linear scan using structural `equal()` over all T existing PathTarget
expressions.
Total cost: O(E·T), same pattern as postgresql-0006 but operating on a
`PathTarget` rather than a flat tlist.
### Hot callers (from planner.c)
```
make_group_input_target() line 5688
make_partial_grouping_target() line 5774
make_window_input_target() line 6330, 6632
```
All are called during the grouping/window-function planning phase of every
aggregated query.
## Patch (conceptual — C)
Before the foreach loop in `add_new_columns_to_pathtarget`, build a pointer set
of `target->exprs` entries. For each candidate expr, do O(1) pointer lookup
(sufficient for canonical Var nodes); fall back to `list_member` only for
non-canonical nodes.
## Patch file
See `postgresql-0007-add-new-columns-hash.patch`

View file

@ -0,0 +1,67 @@
--- a/src/backend/optimizer/util/tlist.c
+++ b/src/backend/optimizer/util/tlist.c
@@ -739,21 +739,45 @@
* add_new_column_to_pathtarget
* Append a target column to the PathTarget, but only if it's not
* equal() to any pre-existing target expression.
+ *
+ * CWE-407 note: this function is O(T) per call. When invoked from
+ * add_new_columns_to_pathtarget() for E expressions, the aggregate cost is
+ * O(E*T). Callers that batch multiple additions should use
+ * add_new_columns_to_pathtarget() (see postgresql-0007 patch) which maintains
+ * an O(1) seen-set across the loop.
*/
void
add_new_column_to_pathtarget(PathTarget *target, Expr *expr)
{
if (!list_member(target->exprs, expr))
add_column_to_pathtarget(target, expr, 0);
}
/*
* add_new_columns_to_pathtarget
* Apply add_new_column_to_pathtarget() for each element of the list.
+ *
+ * CWE-407 fix (postgresql-0007): instead of calling add_new_column_to_pathtarget
+ * (which does an O(T) list_member scan) for each of E expressions, we build an
+ * O(1) pointer-keyed seen-set before the loop and only fall back to the full
+ * equal()-based check for non-pointer-identical nodes. This reduces total cost
+ * from O(E*T) to O(T + E).
*/
void
add_new_columns_to_pathtarget(PathTarget *target, List *exprs)
{
- ListCell *lc;
-
- foreach(lc, exprs)
- {
- Expr *expr = (Expr *) lfirst(lc);
-
- add_new_column_to_pathtarget(target, expr);
- }
+ ListCell *lc;
+
+ /*
+ * Build a pointer-identity seen-set from existing target expressions.
+ * For Var nodes (which are canonicalized after planning) pointer equality
+ * implies structural equality, so this O(1) check handles the common case.
+ * Non-identical pointers fall through to the full list_member check.
+ */
+ List *seen_ptrs = list_copy(target->exprs);
+
+ foreach(lc, exprs)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+
+ if (list_member_ptr(seen_ptrs, expr))
+ continue; /* O(1): pointer already present */
+
+ /* Pointer miss: fall back to structural equal() check */
+ if (!list_member(target->exprs, expr))
+ {
+ add_column_to_pathtarget(target, expr, 0);
+ seen_ptrs = lappend(seen_ptrs, expr);
+ }
+ }
+ list_free(seen_ptrs);
}