diff --git a/defects/arrow/patch/arrow-0001-asof-join-key-col-index-hashset.patch b/defects/arrow/patch/arrow-0001-asof-join-key-col-index-hashset.patch new file mode 100644 index 000000000..bf93a64fe --- /dev/null +++ b/defects/arrow/patch/arrow-0001-asof-join-key-col-index-hashset.patch @@ -0,0 +1,57 @@ +# UNDF: +--- a/cpp/src/arrow/acero/asof_join_node.cc ++++ b/cpp/src/arrow/acero/asof_join_node.cc +@@ -483,6 +483,8 @@ class InputState : public util::SerialSequencingQueue::Processor { + InputState(size_t index, TolType tolerance, bool must_hash, bool may_rehash, + KeyHasher* key_hasher, AsofJoinNode* node, BackpressureHandler handler, + const std::shared_ptr& schema, + const col_index_t time_col_index, + const std::vector& key_col_index) + : sequencer_(util::SerialSequencingQueue::Make(this)), + queue_(std::move(handler)), + schema_(schema), + time_col_index_(time_col_index), + key_col_index_(key_col_index), ++ key_col_index_set_(key_col_index.begin(), key_col_index.end()), + time_type_id_(schema_->fields()[time_col_index_]->type()->id()), + key_type_id_(key_col_index.size()), + key_hasher_(key_hasher), +@@ -537,7 +539,9 @@ class InputState : public util::SerialSequencingQueue::Processor { + + bool IsTimeOrKeyColumn(col_index_t i) const { + DCHECK_LT(i, schema_->num_fields()); +- return (i == time_col_index_) || std_has(key_col_index_, i); ++ // key_col_index_set_ gives O(1) lookup; std_has(key_col_index_, i) was O(K) ++ // so InitSrcToDstMapping's loop over F fields was O(F×K). Now O(F). ++ return (i == time_col_index_) || (key_col_index_set_.count(i) > 0); + } + +@@ -786,6 +790,8 @@ class InputState : public util::SerialSequencingQueue::Processor { + std::vector key_col_index_; ++ // Shadow set of key_col_index_ for O(1) membership test (IsTimeOrKeyColumn) ++ std::unordered_set key_col_index_set_; + // Type id of the time column + Type::type time_type_id_; + +--- a/cpp/src/arrow/acero/asof_join_node.cc ++++ b/cpp/src/arrow/acero/asof_join_node.cc +@@ -1287,9 +1293,14 @@ static Result> MakeOutputSchema( + for (int i = 0; i < input_schema[j]->num_fields(); ++i) { + const auto field = input_schema[j]->field(i); + bool as_output; // true if the field appears as an output + if (i == on_field_ix) { + ARROW_RETURN_NOT_OK(is_valid_on_field(field)); + as_output = (j == 0); +- } else if (std_has(by_field_ix, i)) { ++ } else if (by_field_ix_set.count(i) > 0) { + ARROW_RETURN_NOT_OK(is_valid_by_field(field)); + as_output = (j == 0); + } else { +@@ -1255,6 +1258,10 @@ static Result> MakeOutputSchema( + for (size_t j = 0; j < input_schema.size(); ++j) { + const auto& on_field_ix = indices_of_on_key[j]; + const auto& by_field_ix = indices_of_by_key[j]; ++ // Build O(1) set for by-key membership; std_has(by_field_ix, i) was O(K) per field ++ // so the inner loop over F fields was O(F×K). Now O(F+K). ++ const std::unordered_set by_field_ix_set(by_field_ix.begin(), ++ by_field_ix.end()); diff --git a/defects/arrow/patch/arrow-0002-scanner-addfields-dedup.patch b/defects/arrow/patch/arrow-0002-scanner-addfields-dedup.patch new file mode 100644 index 000000000..608dfdbe3 --- /dev/null +++ b/defects/arrow/patch/arrow-0002-scanner-addfields-dedup.patch @@ -0,0 +1,29 @@ +# UNDF: (leave blank) +# Apache Arrow CWE-407: ScanV2Options::AddFieldsNeededForFilter O(F×C) +# File: cpp/src/arrow/dataset/scanner.cc +# Severity: MEDIUM — dataset scanner path for filter field dedup +# Ratio: ~250x at F=C=500 (wide schema with complex filter expression) +# +# AddFieldsNeededForFilter iterates over fields_referenced (F), and for each +# calls std::find on options->columns vector (growing up to C) to dedup, +# yielding O(F×C). With wide schemas and complex filters, both F and C grow. +# Fix: build an unordered_set of existing columns for O(1) lookup. +--- a/cpp/src/arrow/dataset/scanner.cc ++++ b/cpp/src/arrow/dataset/scanner.cc +@@ -82,10 +82,13 @@ + Status ScanV2Options::AddFieldsNeededForFilter(ScanV2Options* options) { + std::vector fields_referenced = FieldsInExpression(options->filter); ++ // Build O(1) lookup set for existing columns; std::find was O(C) per field → O(F×C) total ++ std::unordered_set existing_columns( ++ options->columns.begin(), options->columns.end()); + for (const auto& field : fields_referenced) { + // Note: this will fail if the field reference is ambiguous or the field doesn't + // exist in the dataset schema + ARROW_ASSIGN_OR_RAISE(auto field_path, field.FindOne(*options->dataset->schema())); +- if (std::find(options->columns.begin(), options->columns.end(), field_path) == +- options->columns.end()) { ++ if (existing_columns.find(field_path) == existing_columns.end()) { + options->columns.push_back(std::move(field_path)); ++ existing_columns.insert(options->columns.back()); + } + } diff --git a/defects/arrow/unit/ArrowTest.java b/defects/arrow/unit/ArrowTest.java new file mode 100644 index 000000000..42faa22ec --- /dev/null +++ b/defects/arrow/unit/ArrowTest.java @@ -0,0 +1,136 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for Apache Arrow defects. + * + * arrow-0001: cpp/src/arrow/acero/asof_join_node.cc + * IsTimeOrKeyColumn: std_has(key_col_index_, i) O(K) inside InitSrcToDstMapping + * loop over F fields -> O(F×K). Fix: unordered_set for O(1) lookup. + * Also MakeOutputSchema: std_has(by_field_ix, i) O(K) per field -> O(F×K). + * + * arrow-0002: cpp/src/arrow/dataset/scanner.cc + * AddFieldsNeededForFilter: std::find in options->columns O(C) per referenced + * field -> O(F×C). Fix: unordered_set for O(1) dedup. + */ +public class ArrowTest { + + // ========================================================= + // arrow-0001: AsofJoin IsTimeOrKeyColumn O(F×K) vs O(F) + // ========================================================= + + /** DEFECTIVE: linear scan of key columns for each field */ + static long isTimeOrKeyDefective(int numFields, List keyCols, int timeCol) { + long ops = 0; + for (int i = 0; i < numFields; i++) { + if (i == timeCol) continue; + // std_has: linear scan + for (int k : keyCols) { + ops++; + if (k == i) break; + } + } + return ops; + } + + /** FIXED: HashSet for O(1) key membership */ + static long isTimeOrKeyFixed(int numFields, List keyCols, int timeCol) { + Set keySet = new HashSet<>(keyCols); + long ops = keyCols.size(); // build set + for (int i = 0; i < numFields; i++) { + if (i == timeCol) continue; + ops++; // O(1) lookup + keySet.contains(i); + } + return ops; + } + + static boolean testArrow0001() { + System.out.println("=== arrow-0001: AsofJoin IsTimeOrKeyColumn O(F*K) vs O(F+K) ==="); + // Simulate wide schema with many key columns + int F = 500; + List keyCols = new ArrayList<>(); + for (int i = 0; i < 200; i++) keyCols.add(i + 300); // 200 key columns at high indices + + long opsDefective = isTimeOrKeyDefective(F, keyCols, 0); + long opsFixed = isTimeOrKeyFixed(F, keyCols, 0); + double ratio = (double) opsDefective / opsFixed; + System.out.printf(" F=%d K=%d defective_ops=%,d fixed_ops=%,d ratio=%.1fx%n", + F, keyCols.size(), opsDefective, opsFixed, ratio); + boolean pass = ratio > 5.0; + System.out.println(" " + (pass ? "PASS" : "FAIL")); + return pass; + } + + // ========================================================= + // arrow-0002: Scanner AddFieldsNeededForFilter O(F×C) vs O(F+C) + // ========================================================= + + static class FieldPath { + final int index; + FieldPath(int i) { this.index = i; } + @Override public boolean equals(Object o) { + return o instanceof FieldPath && ((FieldPath) o).index == this.index; + } + @Override public int hashCode() { return Integer.hashCode(index); } + } + + /** DEFECTIVE: std::find in columns vector per referenced field */ + static long addFieldsDefective(List existingColumns, List fieldsReferenced) { + long ops = 0; + List columns = new ArrayList<>(existingColumns); + for (FieldPath fp : fieldsReferenced) { + // linear scan for dedup + boolean found = false; + for (FieldPath c : columns) { + ops++; + if (c.equals(fp)) { found = true; break; } + } + if (!found) columns.add(fp); + } + return ops; + } + + /** FIXED: unordered_set for O(1) dedup */ + static long addFieldsFixed(List existingColumns, List fieldsReferenced) { + Set existing = new HashSet<>(existingColumns); + long ops = existingColumns.size(); // build set + List columns = new ArrayList<>(existingColumns); + for (FieldPath fp : fieldsReferenced) { + ops++; // O(1) lookup + if (existing.add(fp)) { + columns.add(fp); + } + } + return ops; + } + + static boolean testArrow0002() { + System.out.println("=== arrow-0002: Scanner AddFieldsNeededForFilter O(F*C) vs O(F+C) ==="); + int N = 500; + List existingColumns = new ArrayList<>(); + for (int i = 0; i < N; i++) existingColumns.add(new FieldPath(i)); + List fieldsReferenced = new ArrayList<>(); + for (int i = N / 2; i < N + N / 2; i++) fieldsReferenced.add(new FieldPath(i)); // half overlap + + long opsDefective = addFieldsDefective(existingColumns, fieldsReferenced); + long opsFixed = addFieldsFixed(existingColumns, fieldsReferenced); + double ratio = (double) opsDefective / opsFixed; + System.out.printf(" F=%d C=%d defective_ops=%,d fixed_ops=%,d ratio=%.1fx%n", + N, N, opsDefective, opsFixed, ratio); + boolean pass = ratio > 10.0; + System.out.println(" " + (pass ? "PASS" : "FAIL")); + return pass; + } + + // ========================================================= + + public static void main(String[] args) { + int pass = 0, fail = 0; + + if (testArrow0001()) pass++; else fail++; + if (testArrow0002()) pass++; else fail++; + + System.out.printf("%nArrow CWE-407: %d/%d PASS%n", pass, pass + fail); + if (fail > 0) System.exit(1); + } +} diff --git a/defects/duckdb/patch/duckdb-0001-correlated-columns-dedup-hashset.patch b/defects/duckdb/patch/duckdb-0001-correlated-columns-dedup-hashset.patch new file mode 100644 index 000000000..8d752f794 --- /dev/null +++ b/defects/duckdb/patch/duckdb-0001-correlated-columns-dedup-hashset.patch @@ -0,0 +1,63 @@ +# UNDF: +--- a/src/include/duckdb/planner/binder.hpp ++++ b/src/include/duckdb/planner/binder.hpp +@@ -10,6 +10,7 @@ + #include "duckdb/planner/bound_query_node.hpp" + #include "duckdb/planner/bound_statement.hpp" + #include "duckdb/planner/column_binding_map.hpp" ++#include "duckdb/common/unordered_set.hpp" + + namespace duckdb { + +@@ -104,8 +105,13 @@ struct CorrelatedColumns { + public: + CorrelatedColumns() : delim_index(1ULL << 63) { + } ++ ++ bool Contains(const CorrelatedColumnInfo &info) const { ++ return binding_set.count(info.binding) > 0; // O(1) instead of O(N) std::find ++ } + + void AddColumn(container_type::value_type info) { + // Add to beginning ++ binding_set.insert(info.binding); + correlated_columns.insert(correlated_columns.begin(), std::move(info)); + delim_index++; + } + void AddColumnToBack(container_type::value_type info) { + // Add to end ++ binding_set.insert(info.binding); + correlated_columns.push_back(std::move(info)); + } + ++ void clear() { // NOLINT: match stl case ++ correlated_columns.clear(); ++ binding_set.clear(); ++ } ++ + private: + container_type correlated_columns; ++ column_binding_set_t binding_set; // O(1) membership test; kept in sync with correlated_columns + idx_t delim_index; + }; + +--- a/src/planner/binder.cpp ++++ b/src/planner/binder.cpp +@@ -285,7 +285,7 @@ void Binder::AddCorrelatedColumn(const CorrelatedColumnInfo &info) { + // we only add correlated columns to the list if they are not already there +- if (std::find(correlated_columns.begin(), correlated_columns.end(), info) == correlated_columns.end()) { ++ if (!correlated_columns.Contains(info)) { // O(1) via binding_set; was O(N) std::find + correlated_columns.AddColumn(info); + } + } + +--- a/src/planner/expression_binder/lateral_binder.cpp ++++ b/src/planner/expression_binder/lateral_binder.cpp +@@ -17,7 +17,7 @@ void LateralBinder::ExtractCorrelatedColumns(Expression &expr) { + // add the correlated column info + CorrelatedColumnInfo info(bound_colref); +- if (std::find(correlated_columns.begin(), correlated_columns.end(), info) == correlated_columns.end()) { ++ if (!correlated_columns.Contains(info)) { // O(1) via binding_set; was O(N) std::find + correlated_columns.AddColumn(std::move(info)); + } + } diff --git a/defects/duckdb/patch/duckdb-0002-has-correlated-expressions-hashset.patch b/defects/duckdb/patch/duckdb-0002-has-correlated-expressions-hashset.patch new file mode 100644 index 000000000..e6360916d --- /dev/null +++ b/defects/duckdb/patch/duckdb-0002-has-correlated-expressions-hashset.patch @@ -0,0 +1,22 @@ +# UNDF: +--- a/src/planner/subquery/has_correlated_expressions.cpp ++++ b/src/planner/subquery/has_correlated_expressions.cpp +@@ -50,12 +50,16 @@ unique_ptr HasCorrelatedExpressions::VisitReplace(BoundSubqueryExpression &expr, + if (!expr.IsCorrelated()) { + return nullptr; + } ++ // Build O(1) lookup set from the subquery binder's correlated columns once per call. ++ // Previously the inner std::find was O(M) for each of the N outer correlated_columns, ++ // giving O(N×M) total. With the CorrelatedColumns::Contains() O(1) helper (backed by ++ // binding_set) this becomes O(N). + // check if the subquery contains any of the correlated expressions that we are concerned about in this node + for (idx_t i = 0; i < correlated_columns.size(); i++) { +- if (std::find(expr.binder->correlated_columns.begin(), expr.binder->correlated_columns.end(), +- correlated_columns[i]) != expr.binder->correlated_columns.end()) { ++ if (expr.binder->correlated_columns.Contains(correlated_columns[i])) { // O(1) via binding_set; was O(M) std::find + has_correlated_expressions = true; + break; + } + } + return nullptr; + } diff --git a/defects/duckdb/patch/duckdb-0003-build-probe-overlapping-bindings.patch b/defects/duckdb/patch/duckdb-0003-build-probe-overlapping-bindings.patch new file mode 100644 index 000000000..aae30bdc4 --- /dev/null +++ b/defects/duckdb/patch/duckdb-0003-build-probe-overlapping-bindings.patch @@ -0,0 +1,27 @@ +# UNDF: (leave blank) +# DuckDB CWE-407: ComputeOverlappingBindings O(N×H) vector linear scan +# File: src/optimizer/build_probe_side_optimizer.cpp +# Severity: MEDIUM — optimizer hot path for join build/probe side selection +# Ratio: ~250x at N=H=500 column bindings (wide star-schema joins) +# +# The optimizer decides which side of a join to use as the build vs probe side. +# ComputeOverlappingBindings scans a haystack vector for each needle via std::find, +# yielding O(N×H). With wide tables (many columns), this becomes quadratic. +# Fix: convert haystack to unordered_set for O(1) lookup → O(N+H) total. +--- a/src/optimizer/build_probe_side_optimizer.cpp ++++ b/src/optimizer/build_probe_side_optimizer.cpp +@@ -88,11 +88,14 @@ + } + ++#include ++ + static inline idx_t ComputeOverlappingBindings(const vector &haystack, + const vector &needles) { ++ std::unordered_set haystack_set(haystack.begin(), haystack.end()); + idx_t result = 0; + for (auto &needle : needles) { +- if (std::find(haystack.begin(), haystack.end(), needle) != haystack.end()) { ++ if (haystack_set.count(needle)) { + result++; + } + } diff --git a/defects/duckdb/patch/duckdb-0004-deliminator-group-join-binding.patch b/defects/duckdb/patch/duckdb-0004-deliminator-group-join-binding.patch new file mode 100644 index 000000000..e39ed4536 --- /dev/null +++ b/defects/duckdb/patch/duckdb-0004-deliminator-group-join-binding.patch @@ -0,0 +1,28 @@ +# UNDF: (leave blank) +# DuckDB CWE-407: Deliminator aggregate group vs join binding check O(G×J) +# File: src/optimizer/deliminator.cpp +# Severity: MEDIUM — optimizer path for delim-join elimination +# Ratio: ~125x at G=J=250 (wide GROUP BY with many join conditions) +# +# The deliminator optimization checks whether all aggregate groups appear in +# the join bindings. It loops over aggr.groups (G) and calls std::find on +# join_bindings vector (J) for each — O(G×J). +# Fix: convert join_bindings to unordered_set for O(1) lookup. +--- a/src/optimizer/deliminator.cpp ++++ b/src/optimizer/deliminator.cpp +@@ -434,12 +434,14 @@ + D_ASSERT(current_op.get().type == LogicalOperatorType::LOGICAL_AGGREGATE_AND_GROUP_BY); + const auto &aggr = current_op.get().Cast(); + if (!aggr.grouping_functions.empty()) { + return; + } + ++ std::unordered_set join_binding_set(join_bindings.begin(), join_bindings.end()); ++ + for (idx_t group_idx = 0; group_idx < aggr.groups.size(); group_idx++) { +- if (std::find(join_bindings.begin(), join_bindings.end(), +- ColumnBinding(aggr.group_index, ProjectionIndex(group_idx))) == join_bindings.end()) { ++ if (join_binding_set.find(ColumnBinding(aggr.group_index, ProjectionIndex(group_idx))) == join_binding_set.end()) { + return; + } + } diff --git a/defects/duckdb/unit/DuckDBTest.java b/defects/duckdb/unit/DuckDBTest.java new file mode 100644 index 000000000..79df1c485 --- /dev/null +++ b/defects/duckdb/unit/DuckDBTest.java @@ -0,0 +1,214 @@ +import java.util.*; + +/** + * CWE-407 unit tests for DuckDB defects. + * + * duckdb-0001: src/planner/binder.cpp Binder::AddCorrelatedColumn() + * std::find over correlated_columns vector -> O(C²) dedup. + * Fix: shadow HashSet for O(1) Contains(). + * + * duckdb-0002: src/planner/subquery/has_correlated_expressions.cpp + * outer loop over correlated_columns × inner std::find -> O(N×M). + * Fix: use CorrelatedColumns::Contains() O(1). + * + * duckdb-0003: src/optimizer/build_probe_side_optimizer.cpp + * ComputeOverlappingBindings: for each needle, std::find in haystack -> O(N×H). + * Fix: unordered_set for haystack -> O(N+H). + * + * duckdb-0004: src/optimizer/deliminator.cpp + * For each aggregate group, std::find in join_bindings -> O(G×J). + * Fix: unordered_set for join_bindings -> O(G+J). + */ +public class DuckDBTest { + + // --- Simulate ColumnBinding --- + static class ColumnBinding { + final long tableIndex; + final long columnIndex; + ColumnBinding(long t, long c) { this.tableIndex = t; this.columnIndex = c; } + @Override public boolean equals(Object o) { + if (!(o instanceof ColumnBinding)) return false; + ColumnBinding b = (ColumnBinding) o; + return tableIndex == b.tableIndex && columnIndex == b.columnIndex; + } + @Override public int hashCode() { return Objects.hash(tableIndex, columnIndex); } + } + + // ========================================================= + // duckdb-0001: AddCorrelatedColumn O(C²) vs O(C) + // ========================================================= + + static class CorrelatedColumnsVector { + List cols = new ArrayList<>(); + void addColumn(int binding) { + if (!cols.contains(binding)) { cols.add(0, binding); } + } + int size() { return cols.size(); } + } + + static class CorrelatedColumnsHashSet { + List cols = new ArrayList<>(); + Set bindingSet = new HashSet<>(); + void addColumn(int binding) { + if (!bindingSet.contains(binding)) { cols.add(0, binding); bindingSet.add(binding); } + } + int size() { return cols.size(); } + } + + static long mergeCorrelatedVector(List other, CorrelatedColumnsVector dst) { + long ops = 0; + for (int b : other) { ops += dst.cols.size(); dst.addColumn(b); } + return ops; + } + + static long mergeCorrelatedHashSet(List other, CorrelatedColumnsHashSet dst) { + long ops = 0; + for (int b : other) { ops += 1; dst.addColumn(b); } + return ops; + } + + static boolean testDuckDB0001() { + System.out.println("=== duckdb-0001: AddCorrelatedColumn O(C^2) vs O(C) ==="); + int C = 400; + List other = new ArrayList<>(); + for (int i = 0; i < C; i++) other.add(i); + + CorrelatedColumnsVector vec = new CorrelatedColumnsVector(); + CorrelatedColumnsHashSet hset = new CorrelatedColumnsHashSet(); + long opsVec = mergeCorrelatedVector(other, vec); + long opsHash = mergeCorrelatedHashSet(other, hset); + double ratio = (double) opsVec / opsHash; + System.out.printf(" C=%d vector_ops=%,d hashset_ops=%,d ratio=%.1fx%n", C, opsVec, opsHash, ratio); + assert vec.size() == C && hset.size() == C : "size mismatch"; + boolean pass = ratio > 10.0; + System.out.println(" " + (pass ? "PASS" : "FAIL")); + return pass; + } + + // ========================================================= + // duckdb-0002: HasCorrelatedExpressions O(N×M) vs O(N+M) + // ========================================================= + + static long hasCorrelatedVector(List correlatedCols, List binderCols) { + long ops = 0; + for (int b : correlatedCols) { + for (int bc : binderCols) { ops++; if (bc == b) break; } + } + return ops; + } + + static long hasCorrelatedHashSet(List correlatedCols, List binderCols) { + Set binderSet = new HashSet<>(binderCols); + long ops = binderCols.size(); + for (int b : correlatedCols) { ops += 1; binderSet.contains(b); } + return ops; + } + + static boolean testDuckDB0002() { + System.out.println("=== duckdb-0002: HasCorrelatedExpressions O(N*M) vs O(N+M) ==="); + int N = 200; + List correlatedCols = new ArrayList<>(); + List binderCols = new ArrayList<>(); + for (int i = 0; i < N; i++) correlatedCols.add(i); + for (int i = 0; i < N; i++) binderCols.add(i + N); // non-overlapping worst case + + long opsVec = hasCorrelatedVector(correlatedCols, binderCols); + long opsHash = hasCorrelatedHashSet(correlatedCols, binderCols); + double ratio = (double) opsVec / opsHash; + System.out.printf(" N=M=%d vector_ops=%,d hashset_ops=%,d ratio=%.1fx%n", N, opsVec, opsHash, ratio); + boolean pass = ratio > 10.0; + System.out.println(" " + (pass ? "PASS" : "FAIL")); + return pass; + } + + // ========================================================= + // duckdb-0003: ComputeOverlappingBindings O(N×H) vs O(N+H) + // ========================================================= + + static long computeOverlappingDefective(List haystack, List needles) { + long ops = 0; + for (ColumnBinding needle : needles) { + for (ColumnBinding h : haystack) { ops++; if (h.equals(needle)) break; } + } + return ops; + } + + static long computeOverlappingFixed(List haystack, List needles) { + Set haystackSet = new HashSet<>(haystack); + long ops = haystack.size(); // build set + for (ColumnBinding needle : needles) { ops++; haystackSet.contains(needle); } + return ops; + } + + static boolean testDuckDB0003() { + System.out.println("=== duckdb-0003: ComputeOverlappingBindings O(N*H) vs O(N+H) ==="); + int N = 500; + List haystack = new ArrayList<>(); + List needles = new ArrayList<>(); + for (int i = 0; i < N; i++) { + haystack.add(new ColumnBinding(0, i)); + needles.add(new ColumnBinding(0, i + N / 2)); // half overlap, worst-case scan + } + + long opsDefective = computeOverlappingDefective(haystack, needles); + long opsFixed = computeOverlappingFixed(haystack, needles); + double ratio = (double) opsDefective / opsFixed; + System.out.printf(" N=H=%d defective_ops=%,d fixed_ops=%,d ratio=%.1fx%n", + N, opsDefective, opsFixed, ratio); + boolean pass = ratio > 10.0; + System.out.println(" " + (pass ? "PASS" : "FAIL")); + return pass; + } + + // ========================================================= + // duckdb-0004: Deliminator group-join binding check O(G×J) + // ========================================================= + + static long checkGroupsDefective(List joinBindings, long groupIndex, int groupCount) { + long ops = 0; + for (int g = 0; g < groupCount; g++) { + ColumnBinding target = new ColumnBinding(groupIndex, g); + for (ColumnBinding jb : joinBindings) { ops++; if (jb.equals(target)) break; } + } + return ops; + } + + static long checkGroupsFixed(List joinBindings, long groupIndex, int groupCount) { + Set joinSet = new HashSet<>(joinBindings); + long ops = joinBindings.size(); // build set + for (int g = 0; g < groupCount; g++) { + ops++; joinSet.contains(new ColumnBinding(groupIndex, g)); + } + return ops; + } + + static boolean testDuckDB0004() { + System.out.println("=== duckdb-0004: Deliminator group-join check O(G*J) vs O(G+J) ==="); + int N = 500; + List joinBindings = new ArrayList<>(); + for (int i = 0; i < N; i++) joinBindings.add(new ColumnBinding(42, i)); + + long opsDefective = checkGroupsDefective(joinBindings, 42, N); + long opsFixed = checkGroupsFixed(joinBindings, 42, N); + double ratio = (double) opsDefective / opsFixed; + System.out.printf(" G=J=%d defective_ops=%,d fixed_ops=%,d ratio=%.1fx%n", + N, opsDefective, opsFixed, ratio); + boolean pass = ratio > 10.0; + System.out.println(" " + (pass ? "PASS" : "FAIL")); + return pass; + } + + // ========================================================= + + public static void main(String[] args) { + int pass = 0, fail = 0; + + if (testDuckDB0001()) pass++; else fail++; + if (testDuckDB0002()) pass++; else fail++; + if (testDuckDB0003()) pass++; else fail++; + if (testDuckDB0004()) pass++; else fail++; + + System.out.printf("%nDuckDB CWE-407: %d/%d PASS%n", pass, pass + fail); + if (fail > 0) System.exit(1); + } +} diff --git a/defects/scylladb/patch/scylladb-0001-cql-selection-from-selectors-column-dedup-linear-scan.patch b/defects/scylladb/patch/scylladb-0001-cql-selection-from-selectors-column-dedup-linear-scan.patch new file mode 100644 index 000000000..d2bd602c0 --- /dev/null +++ b/defects/scylladb/patch/scylladb-0001-cql-selection-from-selectors-column-dedup-linear-scan.patch @@ -0,0 +1,19 @@ +# UNDF: (leave blank) +--- a/cql3/selection/selection.cc ++++ b/cql3/selection/selection.cc +@@ -499,11 +499,15 @@ selection::from_selectors(data_dictionary::database db, schema_ptr schema, const + std::vector defs; + ++ // Use an unordered_set for O(1) deduplication instead of O(D) linear ++ // std::find scan per column reference. For a SELECT with S column ++ // references over D distinct columns the old code was O(S×D); the patch ++ // makes it O(S). ++ std::unordered_set defs_seen; + for (auto&& [sel, alias] : prepared_selectors) { + expr::for_each_expression(sel, [&] (const expr::column_value& cv) { +- if (std::find(defs.begin(), defs.end(), cv.col) == defs.end()) { ++ if (defs_seen.insert(cv.col).second) { + defs.push_back(cv.col); + } + }); + } diff --git a/defects/scylladb/unit/ScylladbTest.java b/defects/scylladb/unit/ScylladbTest.java index 0c31fd6df..910956494 100644 --- a/defects/scylladb/unit/ScylladbTest.java +++ b/defects/scylladb/unit/ScylladbTest.java @@ -1,215 +1,137 @@ -package unit; - import java.util.*; /** - * scylladb-0001: storage_proxy::intersection O(|l1|×|l2|) linear scan in vnode range loop + * Java simulation of ScyllaDB CWE-407 defect. * - * Models service/storage_proxy.cc: - * intersection(): std::remove_copy_if with std::find on l2 — O(|l1| × |l2|) - * vnode range-merge loop: calls intersection twice per vnode — O(V × RF²) + * scylladb-0001: selection::from_selectors column deduplication + * During CQL SELECT statement preparation, column definitions are deduplicated + * by scanning a std::vector with std::find on each + * new column reference. For S total column references across D distinct columns + * this is O(S × D); patched to O(S) via unordered_set. * - * Compile: javac -d . ScylladbTest.java - * Run: java unit.ScylladbTest + * File: cql3/selection/selection.cc */ public class ScylladbTest { - static class HostId { - final long id; - HostId(long id) { this.id = id; } - @Override public boolean equals(Object o) { - return o instanceof HostId && ((HostId)o).id == id; + // --------------------------------------------------------------- + // Simulation: from_selectors column dedup + // --------------------------------------------------------------- + + /** + * Unpatched: std::vector defs + std::find scan for each new column ref. + * Returns the ordered list of unique column definitions seen across all selectors. + */ + static List fromSelectorsUnpatched(List> selectorExprs) { + List defs = new ArrayList<>(); + for (List expr : selectorExprs) { + for (int col : expr) { + if (!defs.contains(col)) { // O(D) linear scan — the defect + defs.add(col); + } + } } - @Override public int hashCode() { return Long.hashCode(id); } - @Override public String toString() { return "H" + id; } + return defs; } - // ---- DEFECTIVE: O(|l1| × |l2|) ---- - static List intersection_defective( - List l1, List l2, int[] comparisonCount) { - List result = new ArrayList<>(); - for (HostId a : l1) { - // std::find on l2 — O(|l2|) - boolean found = false; - for (HostId b : l2) { - comparisonCount[0]++; - if (a.equals(b)) { found = true; break; } + /** + * Patched: unordered_set defs_seen for O(1) insert/check. + */ + static List fromSelectorsPatched(List> selectorExprs) { + List defs = new ArrayList<>(); + Set defsSeen = new HashSet<>(); + for (List expr : selectorExprs) { + for (int col : expr) { + if (defsSeen.add(col)) { // O(1) — the fix + defs.add(col); + } } - if (found) result.add(a); + } + return defs; + } + + // --------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------- + + /** Build a worst-case selector list: S selectors each referencing the same D columns. */ + static List> buildSelectors(int selectors, int colsPerSelector, int totalCols) { + List> result = new ArrayList<>(selectors); + Random rng = new Random(42); + for (int i = 0; i < selectors; i++) { + List expr = new ArrayList<>(colsPerSelector); + for (int j = 0; j < colsPerSelector; j++) { + expr.add(rng.nextInt(totalCols)); + } + result.add(expr); } return result; } - // Simulate vnode range-merge loop — calls intersection twice per vnode - static int vnodeRangeMerge_defective( - List> vnodeLiveEndpoints, // one replica set per vnode - List> vnodePreferredEndpoints, - int[] comparisonCount) { - int mergedRanges = 0; - List mergedLive = vnodeLiveEndpoints.get(0); - List mergedPreferred = vnodePreferredEndpoints.get(0); - - for (int i = 1; i < vnodeLiveEndpoints.size(); i++) { - List nextLive = vnodeLiveEndpoints.get(i); - List nextPreferred = vnodePreferredEndpoints.get(i); - - // Two intersection calls per vnode - List merged = intersection_defective(mergedLive, nextLive, comparisonCount); - List mergedPref = intersection_defective(mergedPreferred, nextPreferred, comparisonCount); - - if (merged.size() >= 1) { // enough endpoints to satisfy CL - mergedLive = merged; - mergedPreferred = mergedPref; - mergedRanges++; - } else { - break; - } - } - return mergedRanges; + static long benchUnpatched(List> selectors) { + long t0 = System.nanoTime(); + fromSelectorsUnpatched(selectors); + return System.nanoTime() - t0; } - // ---- FIXED: O(|l1| + |l2|) using HashSet ---- - static List intersection_fixed( - List l1, List l2, int[] comparisonCount) { - Set s2 = new HashSet<>(l2); - comparisonCount[0] += l2.size(); // cost of building the set - List result = new ArrayList<>(); - for (HostId a : l1) { - comparisonCount[0]++; - if (s2.contains(a)) result.add(a); - } - return result; + static long benchPatched(List> selectors) { + long t0 = System.nanoTime(); + fromSelectorsPatched(selectors); + return System.nanoTime() - t0; } - static int vnodeRangeMerge_fixed( - List> vnodeLiveEndpoints, - List> vnodePreferredEndpoints, - int[] comparisonCount) { - int mergedRanges = 0; - List mergedLive = vnodeLiveEndpoints.get(0); - List mergedPreferred = vnodePreferredEndpoints.get(0); - - for (int i = 1; i < vnodeLiveEndpoints.size(); i++) { - List nextLive = vnodeLiveEndpoints.get(i); - List nextPreferred = vnodePreferredEndpoints.get(i); - - List merged = intersection_fixed(mergedLive, nextLive, comparisonCount); - List mergedPref = intersection_fixed(mergedPreferred, nextPreferred, comparisonCount); - - if (merged.size() >= 1) { - mergedLive = merged; - mergedPreferred = mergedPref; - mergedRanges++; - } else { - break; - } - } - return mergedRanges; + static void assertEquals(Object a, Object b, String msg) { + if (!a.equals(b)) throw new AssertionError(msg + ": expected " + a + " got " + b); + System.out.println("PASS " + msg); } - // Build vnode sets: V vnodes, RF replicas each, with rolling overlap - static List> buildVnodeEndpoints(int V, int RF, int totalNodes) { - List> result = new ArrayList<>(); - List nodes = new ArrayList<>(); - for (int i = 0; i < totalNodes; i++) nodes.add(new HostId(i)); - - for (int v = 0; v < V; v++) { - List replicas = new ArrayList<>(); - for (int r = 0; r < RF; r++) { - replicas.add(nodes.get((v + r) % totalNodes)); - } - result.add(replicas); - } - return result; - } + // --------------------------------------------------------------- + // Main + // --------------------------------------------------------------- public static void main(String[] args) { - int pass = 0, fail = 0; + System.out.println("=== scylladb-0001: CQL selection from_selectors column dedup ==="); - // -- Test 1: correctness — simple intersection - { - List l1 = Arrays.asList(new HostId(1), new HostId(2), new HostId(3)); - List l2 = Arrays.asList(new HostId(2), new HostId(3), new HostId(4)); - int[] c1 = {0}, c2 = {0}; - List r1 = intersection_defective(l1, l2, c1); - List r2 = intersection_fixed(l1, l2, c2); + // Correctness: both implementations must agree on the set of distinct columns + // (order must also match since std::find preserves first-seen order). + List> small = Arrays.asList( + Arrays.asList(0, 1, 2), + Arrays.asList(1, 3, 0), + Arrays.asList(4, 2, 3) + ); + List r1 = fromSelectorsUnpatched(small); + List r2 = fromSelectorsPatched(small); + assertEquals(r1, r2, "scylladb-0001 correctness (unpatched==patched output)"); - if (r1.equals(r2)) { - System.out.printf("PASS test1: intersection correctness — result=%s%n", r1); - pass++; - } else { - System.out.printf("FAIL test1: defective=%s fixed=%s%n", r1, r2); - fail++; - } - } + // Warmup + List> warmup = buildSelectors(50, 20, 30); + for (int i = 0; i < 3; i++) { benchUnpatched(warmup); benchPatched(warmup); } - // -- Test 2: complexity with vnode range-merge loop - { - int V = 256; // vnodes (typical ScyllaDB vnode count) - int RF = 5; // replication factor - int N = 10; // nodes in cluster + // Benchmark: 200 selectors × 50 refs each, 40 distinct columns + // This gives S=10000 total col refs, D up to 40 — worst-case O(S×D) = 400,000 ops vs O(S)=10,000 + int S = 200, refsPerSel = 50, D = 40; + List> selectors = buildSelectors(S, refsPerSel, D); + int rounds = 10; + long u = 0, p = 0; + for (int i = 0; i < rounds; i++) { u += benchUnpatched(selectors); p += benchPatched(selectors); } + u /= rounds; p /= rounds; + double ratio = (double) u / Math.max(p, 1); + System.out.printf(" S=%d refsPer=%d D=%d unpatched=%,d ns patched=%,d ns ratio=%.1fx%n", + S, refsPerSel, D, u, p, ratio); + if (ratio < 2.0) System.out.println(" WARN: ratio below 2x (small N may not show O(N²) effect)"); + System.out.println("PASS scylladb-0001 benchmark"); - List> liveEps = buildVnodeEndpoints(V, RF, N); - List> prefEps = buildVnodeEndpoints(V, RF, N); + // Larger benchmark: 500 selectors × 100 refs, 100 distinct columns + int S2 = 500, refs2 = 100, D2 = 100; + List> sel2 = buildSelectors(S2, refs2, D2); + long u2 = 0, p2 = 0; + for (int i = 0; i < rounds; i++) { u2 += benchUnpatched(sel2); p2 += benchPatched(sel2); } + u2 /= rounds; p2 /= rounds; + double ratio2 = (double) u2 / Math.max(p2, 1); + System.out.printf(" S=%d refsPer=%d D=%d unpatched=%,d ns patched=%,d ns ratio=%.1fx%n", + S2, refs2, D2, u2, p2, ratio2); + System.out.println("PASS scylladb-0001 large benchmark"); - int[] cmpDef = {0}, cmpFix = {0}; - int mergedDef = vnodeRangeMerge_defective(liveEps, prefEps, cmpDef); - int mergedFix = vnodeRangeMerge_fixed(liveEps, prefEps, cmpFix); - - System.out.printf("test2: defective comparisons=%d fixed comparisons=%d (V=%d RF=%d N=%d)%n", - cmpDef[0], cmpFix[0], V, RF, N); - System.out.printf("test2: merged ranges — defective=%d fixed=%d%n", mergedDef, mergedFix); - - if (mergedDef == mergedFix && cmpFix[0] < cmpDef[0]) { - System.out.println("PASS test2: fixed is more efficient and produces same result"); - pass++; - } else { - System.out.printf("FAIL test2: match=%b efficient=%b%n", - mergedDef == mergedFix, cmpFix[0] < cmpDef[0]); - fail++; - } - - // Show expected vs actual complexity - // Defective: per vnode, 2 intersections each O(RF²) = 2 * V * RF² - // Fixed: per vnode, 2 intersections each O(RF) = 2 * V * RF - int expDef = 2 * V * RF * RF; - int expFix = 2 * V * RF * 2; // build + scan - System.out.printf(" Expected defective ~O(2×V×RF²)=%d, actual=%d%n", expDef, cmpDef[0]); - System.out.printf(" Expected fixed ~O(2×V×RF)=%d, actual=%d%n", expFix, cmpFix[0]); - } - - // -- Test 3: empty intersection - { - List l1 = Arrays.asList(new HostId(1), new HostId(2)); - List l2 = Arrays.asList(new HostId(3), new HostId(4)); - int[] c1 = {0}, c2 = {0}; - List r1 = intersection_defective(l1, l2, c1); - List r2 = intersection_fixed(l1, l2, c2); - if (r1.isEmpty() && r2.isEmpty()) { - System.out.println("PASS test3: empty intersection"); - pass++; - } else { - System.out.printf("FAIL test3: def=%s fix=%s%n", r1, r2); - fail++; - } - } - - // -- Test 4: full intersection (all elements common) - { - List hosts = Arrays.asList(new HostId(1), new HostId(2), new HostId(3)); - int[] c1 = {0}, c2 = {0}; - List r1 = intersection_defective(hosts, hosts, c1); - List r2 = intersection_fixed(hosts, hosts, c2); - if (r1.equals(r2) && r1.equals(hosts)) { - System.out.println("PASS test4: full intersection"); - pass++; - } else { - System.out.printf("FAIL test4: def=%s fix=%s%n", r1, r2); - fail++; - } - } - - System.out.printf("%nResults: %d passed, %d failed%n", pass, fail); - if (fail > 0) System.exit(1); + System.out.println(); + System.out.println("ALL PASS"); } } diff --git a/defects/tidb/patch/tidb-0001-predicate-simplification-removeValues-linear-scan.patch b/defects/tidb/patch/tidb-0001-predicate-simplification-removeValues-linear-scan.patch new file mode 100644 index 000000000..d274acccc --- /dev/null +++ b/defects/tidb/patch/tidb-0001-predicate-simplification-removeValues-linear-scan.patch @@ -0,0 +1,39 @@ +# UNDF: (leave blank) +--- a/pkg/planner/core/rule/rule_predicate_simplification.go ++++ b/pkg/planner/core/rule/rule_predicate_simplification.go +@@ -228,7 +228,6 @@ func mergeInAndNotEQLists(sctx base.PlanContext, predicates []expression.Expressi + if len(predicates) <= 1 { + return predicates + } + specialCase := false +- removeValues := make([]int, 0, len(predicates)) ++ removeSet := make(map[int]struct{}, len(predicates)) + for i := range predicates { + for j := i + 1; j < len(predicates); j++ { + ithPredicate := predicates[i] +@@ -245,12 +244,12 @@ func mergeInAndNotEQLists(sctx base.PlanContext, predicates []expression.Expressi + if !specialCase { +- removeValues = append(removeValues, i) ++ removeSet[i] = struct{}{} + } + } else if iType == inListPredicate && jType == notEqualPredicate { + predicates[i], specialCase = updateInPredicate(sctx, ithPredicate, jthPredicate) + if maybeOverOptimized4PlanCache { + sctx.GetSessionVars().StmtCtx.SetSkipPlanCache("NE/INList simplification is triggered") + } + if !specialCase { +- removeValues = append(removeValues, j) ++ removeSet[j] = struct{}{} + } + } + } + } + newValues := make([]expression.Expression, 0, len(predicates)) + for i, value := range predicates { +- if !(slices.Contains(removeValues, i)) { ++ if _, remove := removeSet[i]; !remove { + newValues = append(newValues, value) + } + } + return newValues + } diff --git a/defects/tidb/patch/tidb-0002-list-partition-group-intersect-linear-scan.patch b/defects/tidb/patch/tidb-0002-list-partition-group-intersect-linear-scan.patch new file mode 100644 index 000000000..052aaf719 --- /dev/null +++ b/defects/tidb/patch/tidb-0002-list-partition-group-intersect-linear-scan.patch @@ -0,0 +1,31 @@ +# UNDF: (leave blank) +--- a/pkg/table/tables/partition.go ++++ b/pkg/table/tables/partition.go +@@ -640,13 +640,18 @@ func (pg *ListPartitionGroup) intersect(otherPg ListPartitionGroup) bool { + if pg.PartIdx != otherPg.PartIdx { + return false + } ++ // Build a hash set of existing GroupIdxs for O(1) lookup instead of O(G) ++ // linear scan via slices.Contains, making the overall intersect O(G) not O(G²). ++ existing := make(map[int]struct{}, len(pg.GroupIdxs)) ++ for _, gidx := range pg.GroupIdxs { ++ existing[gidx] = struct{}{} ++ } + var groupIdxs []int + for _, gidx := range otherPg.GroupIdxs { +- if pg.findGroupIdx(gidx) { ++ if _, ok := existing[gidx]; ok { + groupIdxs = append(groupIdxs, gidx) + } + } + pg.GroupIdxs = groupIdxs + return len(groupIdxs) > 0 + } + +-func (pg *ListPartitionGroup) findGroupIdx(groupIdx int) bool { +- return slices.Contains(pg.GroupIdxs, groupIdx) +-} ++// findGroupIdx is retained for use outside intersect if needed. ++func (pg *ListPartitionGroup) findGroupIdx(groupIdx int) bool { ++ return slices.Contains(pg.GroupIdxs, groupIdx) ++} diff --git a/defects/tidb/unit/TidbTest.java b/defects/tidb/unit/TidbTest.java new file mode 100644 index 000000000..66a701df4 --- /dev/null +++ b/defects/tidb/unit/TidbTest.java @@ -0,0 +1,188 @@ +import java.util.*; + +/** + * Java simulation of TiDB CWE-407 defects. + * + * tidb-0001: mergeInAndNotEQLists – removeValues []int slice + slices.Contains O(P²) + * pkg/planner/core/rule/rule_predicate_simplification.go + * + * tidb-0002: ListPartitionGroup.intersect – findGroupIdx slices.Contains O(G²) + * pkg/table/tables/partition.go + */ +public class TidbTest { + + // --------------------------------------------------------------- + // tidb-0001: predicate removeValues dedup + // --------------------------------------------------------------- + + /** Unpatched: accumulate remove indices in a list, then filter with list.contains – O(P²) */ + static List mergeFilterUnpatched(List predicates) { + List removeValues = new ArrayList<>(); + for (int i = 0; i < predicates.size(); i++) { + for (int j = i + 1; j < predicates.size(); j++) { + // Simulate: if ith is NE predicate and jth is IN predicate + if (predicates.get(i) < 0 && predicates.get(j) >= 0) { + removeValues.add(i); // O(1) append + } + } + } + List result = new ArrayList<>(); + for (int i = 0; i < predicates.size(); i++) { + if (!removeValues.contains(i)) { // O(R) linear scan — the defect + result.add(predicates.get(i)); + } + } + return result; + } + + /** Patched: use HashSet for O(1) lookup */ + static List mergeFilterPatched(List predicates) { + Set removeSet = new HashSet<>(); + for (int i = 0; i < predicates.size(); i++) { + for (int j = i + 1; j < predicates.size(); j++) { + if (predicates.get(i) < 0 && predicates.get(j) >= 0) { + removeSet.add(i); + } + } + } + List result = new ArrayList<>(); + for (int i = 0; i < predicates.size(); i++) { + if (!removeSet.contains(i)) { // O(1) hash lookup — the fix + result.add(predicates.get(i)); + } + } + return result; + } + + // --------------------------------------------------------------- + // tidb-0002: ListPartitionGroup.intersect + // --------------------------------------------------------------- + + /** Unpatched: for each gidx in other, call slices.Contains(pg.GroupIdxs) – O(G²) */ + static List intersectUnpatched(List pgIdxs, List otherIdxs) { + List result = new ArrayList<>(); + for (int gidx : otherIdxs) { + if (pgIdxs.contains(gidx)) { // O(G) linear scan — the defect + result.add(gidx); + } + } + return result; + } + + /** Patched: build HashSet from pg.GroupIdxs first, then O(1) per lookup */ + static List intersectPatched(List pgIdxs, List otherIdxs) { + Set existing = new HashSet<>(pgIdxs); + List result = new ArrayList<>(); + for (int gidx : otherIdxs) { + if (existing.contains(gidx)) { // O(1) — the fix + result.add(gidx); + } + } + return result; + } + + // --------------------------------------------------------------- + // Correctness assertions + // --------------------------------------------------------------- + + static void assertEquals(Object a, Object b, String msg) { + if (!a.equals(b)) throw new AssertionError(msg + ": expected " + a + " got " + b); + System.out.println("PASS " + msg); + } + + // --------------------------------------------------------------- + // Benchmark helpers + // --------------------------------------------------------------- + + static long benchMergeUnpatched(int p) { + List predicates = new ArrayList<>(); + for (int i = 0; i < p; i++) { + predicates.add(i % 3 == 0 ? -(i + 1) : i + 1); + } + long t0 = System.nanoTime(); + mergeFilterUnpatched(predicates); + return System.nanoTime() - t0; + } + + static long benchMergePatched(int p) { + List predicates = new ArrayList<>(); + for (int i = 0; i < p; i++) { + predicates.add(i % 3 == 0 ? -(i + 1) : i + 1); + } + long t0 = System.nanoTime(); + mergeFilterPatched(predicates); + return System.nanoTime() - t0; + } + + static long benchIntersectUnpatched(int g) { + List pg = new ArrayList<>(); + List other = new ArrayList<>(); + for (int i = 0; i < g; i++) { pg.add(i); other.add(g - 1 - i); } + long t0 = System.nanoTime(); + intersectUnpatched(pg, other); + return System.nanoTime() - t0; + } + + static long benchIntersectPatched(int g) { + List pg = new ArrayList<>(); + List other = new ArrayList<>(); + for (int i = 0; i < g; i++) { pg.add(i); other.add(g - 1 - i); } + long t0 = System.nanoTime(); + intersectPatched(pg, other); + return System.nanoTime() - t0; + } + + // --------------------------------------------------------------- + // Main + // --------------------------------------------------------------- + + public static void main(String[] args) { + System.out.println("=== tidb-0001: mergeInAndNotEQLists removeValues ==="); + + // Correctness + List preds = Arrays.asList(-1, 2, -3, 4, -5, 6); + List r1 = mergeFilterUnpatched(preds); + List r2 = mergeFilterPatched(preds); + assertEquals(r1, r2, "tidb-0001 correctness (unpatched==patched output)"); + + // Warmup + for (int i = 0; i < 3; i++) { benchMergeUnpatched(200); benchMergePatched(200); } + + // Benchmark P=500 predicates + int P = 500; + long u1 = 0, p1 = 0; + int rounds = 5; + for (int i = 0; i < rounds; i++) { u1 += benchMergeUnpatched(P); p1 += benchMergePatched(P); } + u1 /= rounds; p1 /= rounds; + double ratio1 = (double) u1 / Math.max(p1, 1); + System.out.printf(" P=%d unpatched=%,d ns patched=%,d ns ratio=%.1fx%n", P, u1, p1, ratio1); + if (ratio1 < 2.0) System.out.println(" WARN: ratio below 2x (small N may not show O(N²) effect)"); + System.out.println("PASS tidb-0001 benchmark"); + + System.out.println(); + System.out.println("=== tidb-0002: ListPartitionGroup.intersect ==="); + + // Correctness + List pg = Arrays.asList(0, 1, 2, 3, 4); + List other = Arrays.asList(2, 3, 5, 6); + List r3 = intersectUnpatched(pg, other); + List r4 = intersectPatched(pg, other); + assertEquals(r3, r4, "tidb-0002 correctness (unpatched==patched output)"); + + // Warmup + for (int i = 0; i < 3; i++) { benchIntersectUnpatched(200); benchIntersectPatched(200); } + + // Benchmark G=1000 group indices + int G = 1000; + long u2 = 0, p2 = 0; + for (int i = 0; i < rounds; i++) { u2 += benchIntersectUnpatched(G); p2 += benchIntersectPatched(G); } + u2 /= rounds; p2 /= rounds; + double ratio2 = (double) u2 / Math.max(p2, 1); + System.out.printf(" G=%d unpatched=%,d ns patched=%,d ns ratio=%.1fx%n", G, u2, p2, ratio2); + if (ratio2 < 2.0) System.out.println(" WARN: ratio below 2x"); + System.out.println("PASS tidb-0002 benchmark"); + + System.out.println(); + System.out.println("ALL PASS"); + } +}