java-topology/docs/tickets/seaorm-0004-topological-sort-from-iter-seen-vec.md
russell@unturf.com 547a9f5738 ORM wave 2: 10 new defects — Active Record +3, Exposed +3, SeaORM +4 (167 sites, 64 ecosystems)
rails-0009: FilterAttributeHandler filter_parameters Array O(A×F) → Set (450×)
rails-0010: Encryption::AutoFilteredParameters two Array scans → Set (250×)
rails-0011: TimeZoneConversion skip_list Array O(M×C×S) → Set (20×)

exposed-0001: SchemaUtilityApi mapMissingColumnStatements O(N×M) → map (118×)
exposed-0002: IdentifierManagerApi isAKeyword O(K) linear → HashSet (144×)
exposed-0003: Table.clone consParams.map fresh List → hoisted HashSet (6×)

seaorm-0001: active_model establish_links leftover.any O(N²) → HashSet (501×)
seaorm-0002: rbac engine group_permissions .values().find() → HashMap by ID (502×)
seaorm-0003: schema builder sorted_tables Vec::contains → HashSet (500×)
seaorm-0004: TopologicalSort from_iter seen Vec O(N²) → BTreeSet (28×)

Unit tests: RailsTest 11/11, ExposedTest 3/3, SeaORMTest 4/4 PASS
Whitepaper: 157→167 sites, 62→64 ecosystems; §13.12 ORM Wave 2 added
2026-03-27 13:49:46 -04:00

2.1 KiB
Raw Permalink Blame History

seaorm-0004: TopologicalSort::from_iter seen Vec — O(N²) scan

Severity: MEDIUM File: src/schema/topology.rs Line: 213228 Status: PATCHED

Description

TopologicalSort<T> implements FromIterator<T> via a seen: Vec<T> that accumulates every processed item. For each new item the impl iterates the full seen list:

let mut seen = Vec::<T>::default();
for item in iter {
    let _ = top.insert(item.clone());
    for seen_item in seen.iter().cloned() {   // O(|seen|) per iteration
        match seen_item.partial_cmp(&item) { ... }
    }
    seen.push(item);
}

With N items in the input iterator the total number of comparisons is N*(N-1)/2, giving O(N²) time.

from_iter is called by sorted_tables (schema builder) to order entity tables before DDL is emitted. An application with 500 registered entities triggers ~125 000 comparisons where O(N log N) is achievable.

Root Cause

seen accumulates all previous items so that ordering relationships can be inferred at insertion time, but each insertion scans the full history rather than using a sorted or indexed structure.

Fix

Replace the Vec with a BTreeSet or sorted Vec so the inner scan can be replaced with a single binary-search-based bounds check:

let mut seen: std::collections::BTreeSet<T> = Default::default();
for item in iter {
    let _ = top.insert(item.clone());
    // all items in seen that are Less become predecessors of item
    for seen_item in seen.range(..item.clone()).cloned().collect::<Vec<_>>() {
        top.add_dependency(seen_item, item.clone());
    }
    // all items in seen that are Greater become successors
    for seen_item in seen.range((std::ops::Bound::Excluded(item.clone()), ..))
                         .cloned().collect::<Vec<_>>() {
        top.add_dependency(item.clone(), seen_item);
    }
    seen.insert(item);
}

This reduces the per-insertion work from O(N) to O(log N + K) where K is the number of actual ordering edges added, giving O(N log N) overall.

Speedup

~50x at N=1000 (500 000 comparisons → ~10 000 range queries)