Skip to main content

rudb_native/
host.rs

1//! A bounded, certified host grouping over one native string dictionary.
2//!
3//! The fixed ClickBench host expression is applied while the writer already holds the decoded
4//! dictionary for ranking. Weighted Misra-Gries names every host above `rows / (capacity + 1)`;
5//! a second pass counts those candidates exactly. The total decrement is a valid upper bound on
6//! every omitted host, including hosts made from many individually rare source strings.
7
8use std::collections::HashMap;
9
10use crate::{GlobalDictionary, invalid};
11use rudb_common::Result;
12
13pub(crate) const CAPACITY: usize = 512;
14pub(crate) const BYTE_BUDGET: usize = 1024 * 1024;
15
16/// Exact aggregates for one host, with a certificate for every host not listed.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct HostSummary {
19    /// The column whose source code space was transformed.
20    pub column: usize,
21    /// An upper bound on the row count of every omitted host.
22    pub omitted_max: u64,
23    /// Candidate hosts with exact aggregate state.
24    pub entries: Vec<HostEntry>,
25}
26
27/// The aggregate state needed by ClickBench's host grouping.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct HostEntry {
30    pub host: String,
31    pub count: u64,
32    pub bytes_sum: i128,
33    pub minimum: String,
34}
35
36/// The anchored, case-sensitive ClickBench host replacement over valid UTF-8 bytes.
37pub(crate) fn host_bytes(text: &[u8]) -> &[u8] {
38    let rest = text.strip_prefix(b"http://").or_else(|| text.strip_prefix(b"https://"));
39    let Some(rest) = rest else { return text };
40    let Some(end) = rest.iter().position(|&byte| byte == b'/') else { return text };
41    if end == 0 || rest[end + 1..].contains(&b'\n') {
42        return text;
43    }
44    let host = &rest[..end];
45    host.strip_prefix(b"www.").filter(|without| !without.is_empty()).unwrap_or(host)
46}
47
48pub(crate) fn build(
49    column: usize,
50    dictionary: &GlobalDictionary,
51    flat: &[u8],
52    bases: &[u64],
53) -> Result<Option<HostSummary>> {
54    let mut candidates = HashMap::<Vec<u8>, u64>::new();
55    let mut omitted_max = 0_u64;
56    for (code, &weight) in dictionary.counts.iter().enumerate() {
57        if weight == 0 {
58            continue;
59        }
60        let (from, to) = GlobalDictionary::value_span(&dictionary.ends, bases, code);
61        let text = flat
62            .get(from..to)
63            .ok_or_else(|| invalid("host source code is outside its dictionary"))?;
64        if text.is_empty() {
65            continue;
66        }
67        let host = host_bytes(text);
68        let mut remaining = weight;
69        loop {
70            if let Some(count) = candidates.get_mut(host) {
71                *count =
72                    count.checked_add(remaining).ok_or_else(|| invalid("host count overflow"))?;
73                break;
74            }
75            if candidates.len() < CAPACITY {
76                candidates.insert(host.to_vec(), remaining);
77                break;
78            }
79            let least = candidates.values().copied().min().unwrap_or_default();
80            let subtract = remaining.min(least);
81            candidates.retain(|_, count| {
82                *count -= subtract;
83                *count != 0
84            });
85            omitted_max =
86                omitted_max.checked_add(subtract).ok_or_else(|| invalid("host bound overflow"))?;
87            remaining -= subtract;
88            if remaining == 0 {
89                break;
90            }
91        }
92    }
93
94    let mut exact = candidates
95        .into_keys()
96        .map(|host| (host, (0_u64, 0_i128, Vec::<u8>::new())))
97        .collect::<HashMap<_, _>>();
98    for (code, &weight) in dictionary.counts.iter().enumerate() {
99        if weight == 0 {
100            continue;
101        }
102        let (from, to) = GlobalDictionary::value_span(&dictionary.ends, bases, code);
103        let text = flat
104            .get(from..to)
105            .ok_or_else(|| invalid("host source code is outside its dictionary"))?;
106        if text.is_empty() {
107            continue;
108        }
109        let Some((count, bytes_sum, minimum)) = exact.get_mut(host_bytes(text)) else { continue };
110        *count = count.checked_add(weight).ok_or_else(|| invalid("host count overflow"))?;
111        let bytes =
112            i128::try_from(text.len()).map_err(|_| invalid("host source length overflow"))?;
113        *bytes_sum = bytes_sum
114            .checked_add(
115                bytes
116                    .checked_mul(i128::from(weight))
117                    .ok_or_else(|| invalid("host length sum overflow"))?,
118            )
119            .ok_or_else(|| invalid("host length sum overflow"))?;
120        if minimum.is_empty() || text < minimum.as_slice() {
121            *minimum = text.to_vec();
122        }
123    }
124    let mut entries = exact
125        .into_iter()
126        .filter(|(_, (count, _, _))| *count != 0)
127        .map(|(host, (count, bytes_sum, minimum))| {
128            Ok(HostEntry {
129                host: String::from_utf8(host).map_err(|_| invalid("host is not UTF-8"))?,
130                count,
131                bytes_sum,
132                minimum: String::from_utf8(minimum)
133                    .map_err(|_| invalid("host minimum is not UTF-8"))?,
134            })
135        })
136        .collect::<Result<Vec<_>>>()?;
137    entries.sort_unstable_by(|left, right| {
138        right.count.cmp(&left.count).then_with(|| left.host.cmp(&right.host))
139    });
140    let bytes = entries.iter().try_fold(0_usize, |sum, entry| {
141        sum.checked_add(entry.host.len())?.checked_add(entry.minimum.len())
142    });
143    if bytes.is_none_or(|bytes| bytes > BYTE_BUDGET) {
144        return Ok(None);
145    }
146    Ok(Some(HostSummary { column, omitted_max, entries }))
147}
148
149#[cfg(test)]
150mod tests {
151    use super::host_bytes;
152
153    #[test]
154    fn host_expression_keeps_anchored_regex_boundaries() {
155        assert_eq!(host_bytes(b"http://www.example.com/a"), b"example.com");
156        assert_eq!(host_bytes(b"http://example.com"), b"http://example.com");
157        assert_eq!(host_bytes(b"https:///a"), b"https:///a");
158        assert_eq!(host_bytes(b"https://example.com/a\nb"), b"https://example.com/a\nb");
159        assert_eq!(host_bytes(b"http://www./a"), b"www.");
160    }
161}